diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 000000000..81a01270a --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "0.27.3", + "commands": [ + "dotnet-csharpier" + ], + "rollForward": false + }, + "swashbuckle.aspnetcore.cli": { + "version": "6.6.2", + "commands": [ + "swagger" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index c8a06e7de..aeb256f8b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,6 +2,7 @@ root = true [*] +file_header_template = SPDX-License-Identifier: Apache-2.0\nLicensed to the Ed-Fi Alliance under one or more agreements.\nThe Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.\nSee the LICENSE and NOTICES files in the project root for more information. indent_style=space indent_size=4 tab_width=2 @@ -34,19 +35,120 @@ resharper_redundant_base_qualifier_highlighting=warning resharper_suggest_var_or_type_built_in_types_highlighting=hint resharper_suggest_var_or_type_elsewhere_highlighting=hint resharper_suggest_var_or_type_simple_types_highlighting=hint -# Microsoft .NET properties -csharp_new_line_before_members_in_object_initializers=false -csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion -csharp_style_var_elsewhere=true:suggestion -csharp_style_var_for_built_in_types=true:suggestion -csharp_style_var_when_type_is_apparent=true:suggestion -dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:none -dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:none -dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:none -dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion -dotnet_style_predefined_type_for_member_access=true:suggestion -dotnet_style_qualification_for_event=false:suggestion -dotnet_style_qualification_for_field=false:suggestion -dotnet_style_qualification_for_method=false:suggestion -dotnet_style_qualification_for_property=false:suggestion -dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion +# Standard Ed-Fi C# settings below +indent_style = space +indent_size = 4 +tab_width = 4 +csharp_new_line_before_members_in_object_initializers = false +csharp_preferred_modifier_order = private, public, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion + +dotnet_diagnostic.IDE0073.severity=error # File header template required + +# Formatting rules +dotnet_diagnostic.IDE0055.severity=warning +csharp_new_line_before_open_brace = all # Allman style +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = no_change +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = true +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_parentheses = none +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_dot = false +csharp_space_before_dot = false +csharp_space_after_semicolon_in_for_statement = true +csharp_space_before_semicolon_in_for_statement = false +csharp_space_around_declaration_statements = false +csharp_space_before_open_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_square_brackets = false +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +# Naming rules +dotnet_naming_rule.local_constants_rule.severity = warning +dotnet_naming_rule.local_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.local_constants_rule.symbols = local_constants_symbols +dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.lower_camel_case_style.required_prefix = _ +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities = * +dotnet_naming_symbols.local_constants_symbols.applicable_kinds = local +dotnet_naming_symbols.local_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly + +# Misc style +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# We *like* var +dotnet_diagnostic.IDE0008.severity=none # IDE0008: Use explicit type +csharp_style_var_elsewhere = false:none +csharp_style_var_for_built_in_types = false:suggestion + +# Using statements +csharp_using_directive_placement=outside_namespace:warning +dotnet_diagnostic.IDE0065.severity=warning # placement of using statements +dotnet_diagnostic.IDE0005.severity=suggestion # Remove unnecessary using directives + +# Lower the priority +dotnet_diagnostic.S4136.severity=suggestion # Method overloads should be grouped together +dotnet_diagnostic.S1135.severity=suggestion # Complete TODO comments +dotnet_diagnostic.S112.severity=suggestion # 'System.Exception' should not be thrown by user code. + +# Allow empty records for discriminated union types +dotnet_diagnostic.S2094.severity=none # S2094: Classes should not be empty + +[**/tests/**/*.cs] +# Allow our strange test class naming convention +dotnet_naming_symbols.amt_tests.applicable_kinds = class,method +dotnet_naming_symbols.amt_tests.word_separator = "_" +dotnet_naming_symbols.amt_tests.capitalization = first_word_upper +dotnet_diagnostic.IDE1006.severity=none +dotnet_diagnostic.S101.severity=none + +# SonarLint doesn't understand implied assertions from FakeItEasy +dotnet_diagnostic.S2699.severity=none # S2699: Tests should include assertions diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..aa8b9a08d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +blank_issues_enabled: false +contact_links: + - name: 🏠 Ed-Fi Community Hub + url: https://community.ed-fi.org + about: For technical support, questions, and community discussions + - name: 📚 Ed-Fi Documentation + url: https://docs.ed-fi.org + about: Official Ed-Fi technical documentation and guides diff --git a/.github/ISSUE_TEMPLATE/engineering-issue.yml b/.github/ISSUE_TEMPLATE/engineering-issue.yml new file mode 100644 index 000000000..c008e2548 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/engineering-issue.yml @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +name: "🔧 Engineering Team Issue" +description: "Internal issue for Ed-Fi engineering team members only" +title: "Feature: [Brief Description of the Issue]" +labels: [] +assignees: [] +body: + - type: markdown + attributes: + value: | + ## ⚠️ Engineering Team Only + + This template is for Ed-Fi engineering team members only. + + If you're not part of the engineering team, please use the [Ed-Fi Community Hub](https://community.ed-fi.org) for support. + + - type: textarea + id: description + attributes: + label: "Issue Description" + description: "Describe the issue or task" + placeholder: "Clear description of the issue..." + validations: + required: true + + - type: textarea + id: acceptance-criteria + attributes: + label: "Acceptance Criteria" + description: "What needs to be done to resolve this issue?" + placeholder: "- [ ] Criteria 1\n- [ ] Criteria 2" + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: "Additional Context" + description: "Any additional information, screenshots, or context" + placeholder: "Additional details..." + validations: + required: false + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..bb067b12b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,30 @@ +## General + +* Make only high confidence suggestions when reviewing code changes. +* Never change NuGet.config files unless explicitly asked to. + +## Formatting + +* Apply code-formatting style defined in `.editorconfig`. +* Prefer file-scoped namespace declarations and single-line using directives. +* Insert a newline before the opening curly brace of any code block (e.g., after `if`, `for`, `while`, `foreach`, `using`, `try`, etc.). +* Ensure that the final return statement of a method is on its own line. +* Use pattern matching and switch expressions wherever possible. +* Use `nameof` instead of string literals when referring to member names. + +### Nullable Reference Types + +* Declare variables non-nullable, and check for `null` at entry points. +* Always use `is null` or `is not null` instead of `== null` or `!= null`. +* Trust the C# null annotations and don't add null checks when the type system says a value cannot be null. + +### Testing + +* We use NUnit tests. +* We use Shouldly for assertions. +* Use FakeItEasy for mocking in tests. +* Copy existing style in nearby files for test method names and capitalization. + +## Running tests + +* To build and run tests in the repo, use the command `./build.ps1 UnitTest` diff --git a/.github/workflows/after-pullrequest.yml b/.github/workflows/after-pullrequest.yml index a95779421..91bb6afe0 100644 --- a/.github/workflows/after-pullrequest.yml +++ b/.github/workflows/after-pullrequest.yml @@ -22,7 +22,6 @@ jobs: permissions: checks: write pull-requests: write - # actions: read # only needed for private repository # contents: read # issues: read diff --git a/.github/workflows/api-e2e-mssql-multitenant.yml b/.github/workflows/api-e2e-mssql-multitenant.yml new file mode 100644 index 000000000..791fb466c --- /dev/null +++ b/.github/workflows/api-e2e-mssql-multitenant.yml @@ -0,0 +1,147 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +name: Admin API Multi Tenant E2E Tests + ODS 7 + Mssql + +on: + push: + branches: [main] + schedule: + - cron: "0 5 * * 1" + workflow_dispatch: + pull_request: + branches: [main] + +env: + JIRA_ACCESS_TOKEN: ${{ secrets.JIRA_ACCESS_TOKEN }} + ADMIN_API_VERSION: "2.2.0" + PROJECT_ID: "13401" + CYCLE_NAME: "Automation Cycle" + TASK_NAME: "API Automation Task" + FOLDER_NAME: "API Automation Run" + RESULTS_FILE: "test-xml-results" + +permissions: read-all + +jobs: + run-e2e-multitenant-tests: + defaults: + run: + working-directory: ./Application/EdFi.Ods.AdminApi/ + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Copy application folder to docker context + run: | + mkdir ../../Docker/Application + cp -r ../EdFi.Ods.AdminApi ../../Docker/Application + + - name: Copy admin api common folder to docker context + run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application + + - name: Copy adminApi V1 folder to docker context + run: cp -r ../EdFi.Ods.AdminApi.V1 ../../Docker/Application + + - name: Copy nuget config to docker context + run: cp ../NuGet.Config ../../Docker/Application + + - name: Update certificates + run: cp -r ../../eng/test-certs/ssl ../../Docker/Settings + + - name: Run Admin API + run: | + docker compose \ + -f '../../Docker/V2/Compose/mssql/MultiTenant/compose-build-dev-multi-tenant.yml' \ + --env-file './E2E Tests/V2/gh-action-setup/.automation_mssql.env' \ + up -d + + - name: Verify containers are running + run: | + chmod +x './E2E Tests/V2/gh-action-setup/admin_inspect.sh' + './E2E Tests/V2/gh-action-setup/admin_inspect.sh' adminapi + + - name: Setup node + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: "20" + + - name: Combine Collections + run: | + npx postman-combine-collections@1.1.2 -f './E2E Tests/V2/Admin API E2E 2.0 -*.postman_collection.json' \ + -n 'Admin API E2E 2.0' \ + -o './E2E Tests/V2/Admin-API-Full-Collection.json' + + - name: Run tests + run: | + npx --package=newman@6.1.3 --package=newman-reporter-htmlextra@1.23.1 newman run './E2E Tests/V2/Admin-API-Full-Collection.json' \ + -e './E2E Tests/V2/Admin API Docker-multitenant-mssql.postman_environment.json' \ + -r cli,junit,htmlextra \ + --reporter-htmlextra-title 'AdminAPI - 2.0' \ + --reporter-htmlextra-export './report-html/results.html' \ + --reporter-junit-export ./reports/report.xml \ + -k + + - name: Get Docker logs + if: failure() + run: | + mkdir docker-logs + docker logs ed-fi-db-admin-adminapi-tenant1 > docker-logs/ed-fi-db-admin-adminapi-tenant1.log + docker logs ed-fi-db-admin-adminapi-tenant2 > docker-logs/ed-fi-db-admin-adminapi-tenant2.log + docker logs adminapi > docker-logs/adminapi.log + + - name: Upload Docker logs + if: failure() + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + with: + name: docker-logs + path: Application/EdFi.Ods.AdminApi/docker-logs/ + retention-days: 5 + + - name: Upload Html results + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + if: success() || failure() + with: + name: test-html-results + path: Application/EdFi.Ods.AdminApi/report-html/results.html + retention-days: 5 + + - name: Upload xml results + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + if: success() || failure() + with: + name: test-xml-results + path: Application/EdFi.Ods.AdminApi/reports/report.xml + retention-days: 5 + + report: + defaults: + run: + shell: pwsh + needs: + - run-e2e-multitenant-tests + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Download artifacts + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8 + + # Commented code until Token is renewed + # - name: Send report to Zephyr + # run: | + # $parameters = @{ + # cycleName = '${{ env.CYCLE_NAME }}' + # taskName = '${{ env.TASK_NAME }}' + # folderName = '${{ env.FOLDER_NAME }}' + # } + # .\eng\send-test-results.ps1 ` + # -PersonalAccessToken ${{ env.JIRA_ACCESS_TOKEN }} ` + # -ProjectId ${{ env.PROJECT_ID }} ` + # -AdminApiVersion '${{ env.ADMIN_API_VERSION }}' ` + # -ResultsFilePath '${{ env.RESULTS_FILE }}/report.xml' ` + # -ConfigParams $parameters diff --git a/.github/workflows/api-e2e-mssql-singletenant.yml b/.github/workflows/api-e2e-mssql-singletenant.yml new file mode 100644 index 000000000..6398af6dd --- /dev/null +++ b/.github/workflows/api-e2e-mssql-singletenant.yml @@ -0,0 +1,146 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +name: Admin API Single Tenant E2E Tests + ODS 7 + Mssql + +on: + push: + branches: [main] + schedule: + - cron: "0 5 * * 1" + workflow_dispatch: + pull_request: + branches: [main] + +env: + JIRA_ACCESS_TOKEN: ${{ secrets.JIRA_ACCESS_TOKEN }} + ADMIN_API_VERSION: "2.2.0" + PROJECT_ID: "13401" + CYCLE_NAME: "Automation Cycle" + TASK_NAME: "API Automation Task" + FOLDER_NAME: "API Automation Run" + RESULTS_FILE: "test-xml-results" + +permissions: read-all + +jobs: + run-e2e-tests: + defaults: + run: + working-directory: ./Application/EdFi.Ods.AdminApi/ + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Copy application folder to docker context + run: | + mkdir ../../Docker/Application + cp -r ../EdFi.Ods.AdminApi ../../Docker/Application + + - name: Copy admin api common folder to docker context + run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application + + - name: Copy adminApi V1 folder to docker context + run: cp -r ../EdFi.Ods.AdminApi.V1 ../../Docker/Application + + - name: Copy nuget config to docker context + run: cp ../NuGet.Config ../../Docker/Application + + - name: Update certificates + run: cp -r ../../eng/test-certs/ssl ../../Docker/Settings + + - name: Run Admin API + run: | + docker compose \ + -f '../../Docker/V2/Compose/mssql/SingleTenant/compose-build-dev.yml' \ + --env-file './E2E Tests/V2/gh-action-setup/.automation_mssql.env' \ + up -d + + - name: Verify containers are running + run: | + chmod +x './E2E Tests/V2/gh-action-setup/admin_inspect.sh' + './E2E Tests/V2/gh-action-setup/admin_inspect.sh' adminapi + + - name: Setup node + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: "16" + + - name: Combine Collections + run: | + npx postman-combine-collections@1.1.2 -f './E2E Tests/V2/Admin API E2E 2.0 -*.postman_collection.json' \ + -n 'Admin API E2E 2.0' \ + -o './E2E Tests/V2/Admin-API-Full-Collection.json' + + - name: Run tests + run: | + npx --package=newman@6.1.3 --package=newman-reporter-htmlextra@1.23.1 newman run './E2E Tests/V2/Admin-API-Full-Collection.json' \ + -e './E2E Tests/V2/Admin API Docker-single-mssql.postman_environment.json' \ + -r cli,junit,htmlextra \ + --reporter-htmlextra-title 'AdminAPI - 2.0' \ + --reporter-htmlextra-export './report-html/results.html' \ + --reporter-junit-export ./reports/report.xml \ + -k + + - name: Get Docker logs + if: failure() + run: | + mkdir docker-logs + docker logs ed-fi-db-admin-adminapi > docker-logs/ed-fi-db-admin-adminapi.log + docker logs adminapi > docker-logs/adminapi.log + + - name: Upload Docker logs + if: failure() + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + with: + name: docker-logs + path: Application/EdFi.Ods.AdminApi/docker-logs/ + retention-days: 5 + + - name: Upload Html results + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + if: success() || failure() + with: + name: test-html-results + path: Application/EdFi.Ods.AdminApi/report-html/results.html + retention-days: 5 + + - name: Upload xml results + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + if: success() || failure() + with: + name: test-xml-results + path: Application/EdFi.Ods.AdminApi/reports/report.xml + retention-days: 5 + + report: + defaults: + run: + shell: pwsh + needs: + - run-e2e-tests + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Download artifacts + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8 + + # Commented code until Token is renewed + # - name: Send report to Zephyr + # run: | + # $parameters = @{ + # cycleName = '${{ env.CYCLE_NAME }}' + # taskName = '${{ env.TASK_NAME }}' + # folderName = '${{ env.FOLDER_NAME }}' + # } + # .\eng\send-test-results.ps1 ` + # -PersonalAccessToken ${{ env.JIRA_ACCESS_TOKEN }} ` + # -ProjectId ${{ env.PROJECT_ID }} ` + # -AdminApiVersion '${{ env.ADMIN_API_VERSION }}' ` + # -ResultsFilePath '${{ env.RESULTS_FILE }}/report.xml' ` + # -ConfigParams $parameters diff --git a/.github/workflows/api-e2e-pgsql-multitenant.yml b/.github/workflows/api-e2e-pgsql-multitenant.yml new file mode 100644 index 000000000..f90d7676a --- /dev/null +++ b/.github/workflows/api-e2e-pgsql-multitenant.yml @@ -0,0 +1,147 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +name: Admin API Multi Tenant E2E Tests + ODS 7 + Pgsql + +on: + push: + branches: [main] + schedule: + - cron: "0 5 * * 1" + workflow_dispatch: + pull_request: + branches: [main] + +env: + JIRA_ACCESS_TOKEN: ${{ secrets.JIRA_ACCESS_TOKEN }} + ADMIN_API_VERSION: "2.2.0" + PROJECT_ID: "13401" + CYCLE_NAME: "Automation Cycle" + TASK_NAME: "API Automation Task" + FOLDER_NAME: "API Automation Run" + RESULTS_FILE: "test-xml-results" + +permissions: read-all + +jobs: + run-e2e-multitenant-tests: + defaults: + run: + working-directory: ./Application/EdFi.Ods.AdminApi/ + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Copy application folder to docker context + run: | + mkdir ../../Docker/Application + cp -r ../EdFi.Ods.AdminApi ../../Docker/Application + + - name: Copy admin api common folder to docker context + run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application + + - name: Copy adminApi V1 folder to docker context + run: cp -r ../EdFi.Ods.AdminApi.V1 ../../Docker/Application + + - name: Copy nuget config to docker context + run: cp ../NuGet.Config ../../Docker/Application + + - name: Update certificates + run: cp -r ../../eng/test-certs/ssl ../../Docker/Settings + + - name: Run Admin API + run: | + docker compose \ + -f '../../Docker/V2/Compose/pgsql/MultiTenant/compose-build-dev-multi-tenant.yml' \ + --env-file './E2E Tests/V2/gh-action-setup/.automation_pgsql.env' \ + up -d + + - name: Verify containers are running + run: | + chmod +x './E2E Tests/V2/gh-action-setup/admin_inspect.sh' + './E2E Tests/V2/gh-action-setup/admin_inspect.sh' adminapi + + - name: Setup node + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: "20" + + - name: Combine Collections + run: | + npx postman-combine-collections@1.1.2 -f './E2E Tests/V2/Admin API E2E 2.0 -*.postman_collection.json' \ + -n 'Admin API E2E 2.0' \ + -o './E2E Tests/V2/Admin-API-Full-Collection.json' + + - name: Run tests + run: | + npx --package=newman@6.1.3 --package=newman-reporter-htmlextra@1.23.1 newman run './E2E Tests/V2/Admin-API-Full-Collection.json' \ + -e './E2E Tests/V2/Admin API Docker-multitenant-pgsql.postman_environment.json' \ + -r cli,junit,htmlextra \ + --reporter-htmlextra-title 'AdminAPI - 2.0' \ + --reporter-htmlextra-export './report-html/results.html' \ + --reporter-junit-export ./reports/report.xml \ + -k + + - name: Get Docker logs + if: failure() + run: | + mkdir docker-logs + docker logs ed-fi-db-admin-adminapi-tenant1 > docker-logs/ed-fi-db-admin-adminapi-tenant1.log + docker logs ed-fi-db-admin-adminapi-tenant2 > docker-logs/ed-fi-db-admin-adminapi-tenant2.log + docker logs adminapi > docker-logs/adminapi.log + + - name: Upload Docker logs + if: failure() + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + with: + name: docker-logs + path: Application/EdFi.Ods.AdminApi/docker-logs/ + retention-days: 5 + + - name: Upload Html results + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + if: success() || failure() + with: + name: test-html-results + path: Application/EdFi.Ods.AdminApi/report-html/results.html + retention-days: 5 + + - name: Upload xml results + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + if: success() || failure() + with: + name: test-xml-results + path: Application/EdFi.Ods.AdminApi/reports/report.xml + retention-days: 5 + + report: + defaults: + run: + shell: pwsh + needs: + - run-e2e-multitenant-tests + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Download artifacts + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8 + + # Commented code until Token is renewed + # - name: Send report to Zephyr + # run: | + # $parameters = @{ + # cycleName = '${{ env.CYCLE_NAME }}' + # taskName = '${{ env.TASK_NAME }}' + # folderName = '${{ env.FOLDER_NAME }}' + # } + # .\eng\send-test-results.ps1 ` + # -PersonalAccessToken ${{ env.JIRA_ACCESS_TOKEN }} ` + # -ProjectId ${{ env.PROJECT_ID }} ` + # -AdminApiVersion '${{ env.ADMIN_API_VERSION }}' ` + # -ResultsFilePath '${{ env.RESULTS_FILE }}/report.xml' ` + # -ConfigParams $parameters diff --git a/.github/workflows/api-e2e-pgsql-singletenant.yml b/.github/workflows/api-e2e-pgsql-singletenant.yml new file mode 100644 index 000000000..3632dc752 --- /dev/null +++ b/.github/workflows/api-e2e-pgsql-singletenant.yml @@ -0,0 +1,142 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +name: Admin API Single Tenant E2E Tests + ODS 7 + Pgsql + +on: + push: + branches: [main] + schedule: + - cron: "0 5 * * 1" + workflow_dispatch: + pull_request: + branches: [main] + +env: + JIRA_ACCESS_TOKEN: ${{ secrets.JIRA_ACCESS_TOKEN }} + ADMIN_API_VERSION: "2.2.0" + PROJECT_ID: "13401" + CYCLE_NAME: "Automation Cycle" + TASK_NAME: "API Automation Task" + FOLDER_NAME: "API Automation Run" + RESULTS_FILE: "test-xml-results" + +permissions: read-all + +jobs: + run-e2e-tests: + defaults: + run: + working-directory: ./Application/EdFi.Ods.AdminApi/ + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Copy application folder to docker context + run: | + mkdir ../../Docker/Application + cp -r ../EdFi.Ods.AdminApi ../../Docker/Application + cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application + cp -r ../EdFi.Ods.AdminApi.V1 ../../Docker/Application + + - name: Copy nuget config to docker context + run: cp ../NuGet.Config ../../Docker/Application + + - name: Update certificates + run: cp -r ../../eng/test-certs/ssl ../../Docker/Settings + + - name: Run Admin API + run: | + docker compose \ + -f '../../Docker/V2/Compose/pgsql/SingleTenant/compose-build-dev.yml' \ + --env-file './E2E Tests/V2/gh-action-setup/.automation_pgsql.env' \ + up -d + + - name: Verify containers are running + run: | + chmod +x './E2E Tests/V2/gh-action-setup/admin_inspect.sh' + './E2E Tests/V2/gh-action-setup/admin_inspect.sh' adminapi + + - name: Setup node + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: "16" + + - name: Combine Collections + run: | + npx postman-combine-collections@1.1.2 -f './E2E Tests/V2/Admin API E2E 2.0 -*.postman_collection.json' \ + -n 'Admin API E2E 2.0' \ + -o './E2E Tests/V2/Admin-API-Full-Collection.json' + + - name: Run tests + run: | + npx --package=newman@6.1.3 --package=newman-reporter-htmlextra@1.23.1 newman run './E2E Tests/V2/Admin-API-Full-Collection.json' \ + -e './E2E Tests/V2/Admin API Docker-single-pgsql.postman_environment.json' \ + -r cli,junit,htmlextra \ + --reporter-htmlextra-title 'AdminAPI - 2.0' \ + --reporter-htmlextra-export './report-html/results.html' \ + --reporter-junit-export ./reports/report.xml \ + -k + + - name: Get Docker logs + if: failure() + run: | + mkdir docker-logs + docker logs ed-fi-db-admin-adminapi > docker-logs/ed-fi-db-admin-adminapi.log + docker logs adminapi > docker-logs/adminapi.log + + - name: Upload Docker logs + if: failure() + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + with: + name: docker-logs + path: Application/EdFi.Ods.AdminApi/docker-logs/ + retention-days: 5 + + - name: Upload Html results + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + if: success() || failure() + with: + name: test-html-results + path: Application/EdFi.Ods.AdminApi/report-html/results.html + retention-days: 5 + + - name: Upload xml results + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + if: success() || failure() + with: + name: test-xml-results + path: Application/EdFi.Ods.AdminApi/reports/report.xml + retention-days: 5 + + report: + defaults: + run: + shell: pwsh + needs: + - run-e2e-tests + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Download artifacts + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8 + + # Commented code until Token is renewed + # - name: Send report to Zephyr + # run: | + # $parameters = @{ + # cycleName = '${{ env.CYCLE_NAME }}' + # taskName = '${{ env.TASK_NAME }}' + # folderName = '${{ env.FOLDER_NAME }}' + # } + # .\eng\send-test-results.ps1 ` + # -PersonalAccessToken ${{ env.JIRA_ACCESS_TOKEN }} ` + # -ProjectId ${{ env.PROJECT_ID }} ` + # -AdminApiVersion '${{ env.ADMIN_API_VERSION }}' ` + # -ResultsFilePath '${{ env.RESULTS_FILE }}/report.xml' ` + # -ConfigParams $parameters diff --git a/.github/workflows/api-e2e-mssql.yml b/.github/workflows/api-v1-e2e-mssql.yml similarity index 89% rename from .github/workflows/api-e2e-mssql.yml rename to .github/workflows/api-v1-e2e-mssql.yml index e92c1324e..71e6d816c 100644 --- a/.github/workflows/api-e2e-mssql.yml +++ b/.github/workflows/api-v1-e2e-mssql.yml @@ -50,14 +50,14 @@ jobs: - name: Run Admin API run: | docker compose \ - -f '../../Docker/Compose/mssql/compose-build-dev.yml' \ - --env-file './E2E Tests/gh-action-setup/.automation.env' \ + -f '../../Docker/V1/Compose/mssql/compose-build-dev.yml' \ + --env-file './E2E Tests/V1/gh-action-setup/.automation.env' \ up -d - name: Verify containers are running run: | - chmod +x './E2E Tests/gh-action-setup/inspect.sh' - './E2E Tests/gh-action-setup/inspect.sh' + chmod +x './E2E Tests/V1/gh-action-setup/inspect.sh' + './E2E Tests/V1/gh-action-setup/inspect.sh' - name: Setup node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -66,14 +66,14 @@ jobs: - name: Combine Collections run: | - npx postman-combine-collections@1.1.2 -f './E2E Tests/Admin API E2E.postman_collection*.json' \ + npx postman-combine-collections@1.1.2 -f './E2E Tests/V1/Admin API E2E.postman_collection*.json' \ -n 'Admin API E2E 1.0' \ - -o './E2E Tests/Admin-API-Full-Collection.json' + -o './E2E Tests/V1/Admin-API-Full-Collection.json' - name: Run tests run: | - npx --package=newman@6.1.2 --package=newman-reporter-htmlextra@1.23.1 -- newman run './E2E Tests/Admin-API-Full-Collection.json' \ - -e './E2E Tests/Admin API Docker.postman_environment.json' \ + npx --package=newman@6.1.2 --package=newman-reporter-htmlextra@1.23.1 -- newman run './E2E Tests/V1/Admin-API-Full-Collection.json' \ + -e './E2E Tests/V1/Admin API Docker.postman_environment.json' \ -r cli,junit,htmlextra \ --reporter-htmlextra-title 'AdminAPI - 1.0' \ --reporter-htmlextra-export './report-html/results.html' \ diff --git a/.github/workflows/api-e2e-pgsql.yml b/.github/workflows/api-v1-e2e-pgsql.yml similarity index 89% rename from .github/workflows/api-e2e-pgsql.yml rename to .github/workflows/api-v1-e2e-pgsql.yml index c4206d4e3..ccb8ddf96 100644 --- a/.github/workflows/api-e2e-pgsql.yml +++ b/.github/workflows/api-v1-e2e-pgsql.yml @@ -50,14 +50,14 @@ jobs: - name: Run Admin API run: | docker compose \ - -f '../../Docker/Compose/pgsql/compose-build-dev.yml' \ - --env-file './E2E Tests/gh-action-setup/.automation.env' \ + -f '../../Docker/V1/Compose/pgsql/compose-build-dev.yml' \ + --env-file './E2E Tests/V1/gh-action-setup/.automation.env' \ up -d - name: Verify containers are running run: | - chmod +x './E2E Tests/gh-action-setup/inspect.sh' - './E2E Tests/gh-action-setup/inspect.sh' + chmod +x './E2E Tests/V1/gh-action-setup/inspect.sh' + './E2E Tests/V1/gh-action-setup/inspect.sh' - name: Setup node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -66,14 +66,14 @@ jobs: - name: Combine Collections run: | - npx postman-combine-collections@1.1.2 -f './E2E Tests/Admin API E2E.postman_collection*.json' \ + npx postman-combine-collections@1.1.2 -f './E2E Tests/V1/Admin API E2E.postman_collection*.json' \ -n 'Admin API E2E 1.0' \ - -o './E2E Tests/Admin-API-Full-Collection.json' + -o './E2E Tests/V1/Admin-API-Full-Collection.json' - name: Run tests run: | - npx --package=newman@6.1.2 --package=newman-reporter-htmlextra@1.23.1 -- newman run './E2E Tests/Admin-API-Full-Collection.json' \ - -e './E2E Tests/Admin API Docker.postman_environment.json' \ + npx --package=newman@6.1.2 --package=newman-reporter-htmlextra@1.23.1 -- newman run './E2E Tests/V1/Admin-API-Full-Collection.json' \ + -e './E2E Tests/V1/Admin API Docker.postman_environment.json' \ -r cli,junit,htmlextra \ --reporter-htmlextra-title 'AdminAPI - 1.0' \ --reporter-htmlextra-export './report-html/results.html' \ diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000..be533e2b5 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +name: Copilot Setup Steps + +on: + workflow_dispatch: + push: + paths: + - ".github/workflows/copilot-setup-steps.yml" + pull_request: + paths: + - ".github/workflows/copilot-setup-steps.yml" + +permissions: + contents: read + +jobs: + copilot-setup-steps: + name: Copilot Setup Steps + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Install NuGet package dependencies + run: dotnet restore Application/Ed-Fi-ODS-AdminApi.sln \ No newline at end of file diff --git a/.github/workflows/on-issue-opened.yml b/.github/workflows/on-issue-opened.yml new file mode 100644 index 000000000..938b97baf --- /dev/null +++ b/.github/workflows/on-issue-opened.yml @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +name: "Issue Management" + +on: + issues: + types: [opened] + +permissions: + issues: write + +jobs: + manage-community-issues: + runs-on: ubuntu-latest + if: github.event.issue.user.type != 'bot' + steps: + - name: Check if user has repository access + id: check-repo-access + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + try { + // Check if user has repository access (collaborator, maintainer, admin, etc.) + const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.payload.issue.user.login + }); + + console.log(`User ${context.payload.issue.user.login} repository permission: ${permission.permission}`); + return ['admin', 'maintain', 'write', 'triage'].includes(permission.permission); + } catch (error) { + console.log(`User ${context.payload.issue.user.login} does not have repository access: ${error.message}`); + return false; + } + + - name: Close community issue with helpful message + if: steps.check-repo-access.outputs.result == 'false' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const message = `Thank you for your interest in Ed-Fi! + + This GitHub repository is maintained by the Ed-Fi engineering team for internal issue tracking and development coordination. + + For technical support, questions, and community discussions, please visit the **[Ed-Fi Community Hub](https://community.ed-fi.org)** where our community and support team can better assist you. + + The Ed-Fi Community Hub provides: + - Technical support and troubleshooting + - Community discussions and best practices + - Documentation and resources + - Direct access to Ed-Fi experts + + We appreciate your understanding and look forward to helping you on the Community Hub!`; + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: message + }); + + await github.rest.issues.update({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + state: 'closed', + state_reason: 'not_planned' + }); diff --git a/.github/workflows/on-prerelease.yml b/.github/workflows/on-prerelease.yml index 456a47745..7c7e4ebbf 100644 --- a/.github/workflows/on-prerelease.yml +++ b/.github/workflows/on-prerelease.yml @@ -11,8 +11,8 @@ on: env: ARTIFACTS_API_KEY: ${{ secrets.AZURE_ARTIFACTS_PERSONAL_ACCESS_TOKEN }} - ARTIFACTS_FEED_URL: "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_packaging/EdFi/nuget/v3/index.json" - VSS_NUGET_EXTERNAL_FEED_ENDPOINTS: '{"endpointCredentials": [{"endpoint": "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_packaging/EdFi/nuget/v3/index.json","password": "${{ secrets.AZURE_ARTIFACTS_PERSONAL_ACCESS_TOKEN }}"}]}' + ARTIFACTS_FEED_URL: ${{ vars.AZURE_ARTIFACTS_FEED_URL }} + VSS_NUGET_EXTERNAL_FEED_ENDPOINTS: '{"endpointCredentials": [{"endpoint": "${{ vars.AZURE_ARTIFACTS_FEED_URL }}","password": "${{ secrets.AZURE_ARTIFACTS_PERSONAL_ACCESS_TOKEN }}"}]}' MANIFEST_FILE: "_manifest/spdx_2.2/manifest.spdx.json" PACKAGE_NAME: "AdminApi" IMAGE_NAME: ${{ vars.IMAGE_NAME }} @@ -63,26 +63,75 @@ jobs: -Configuration Release ` -APIVersion $apiVersion + - name: Get Assembly File Version + id: assembly-version + shell: pwsh + run: | + $dllPath = "./Application/EdFi.Ods.AdminApi/publish/EdFi.Ods.AdminApi.dll" + + if (-not (Test-Path $dllPath)) + { + throw "Could not find EdFi.Ods.AdminApi.dll at $dllPath" + } + + $fileVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($dllPath).FileVersion + "file-version=$fileVersion" >> $env:GITHUB_OUTPUT + - name: Setup Nuget.exe uses: nuget/setup-nuget@323ab0502cd38fdc493335025a96c8fdb0edc71f #v2.0.1 - name: Replace Installer Version Token shell: pwsh run: | - $version = "${{ github.ref_name }}" - $versionWithoutPrefix = $version -replace '^v', '' + $version = "${{ steps.assembly-version.outputs.file-version }}" + + Write-Host "Replacing version token with: $version" $installerPath = "Installer.AdminApi/Install-AdminApi.psm1" - $content = Get-Content $installerPath -Raw - $content = $content -replace '"__ADMINAPI_VERSION__"', "`"$versionWithoutPrefix`"" - $content | Set-Content $installerPath -NoNewline + + if (Test-Path $installerPath) + { + $content = Get-Content $installerPath -Raw + $content = $content -replace '"__ADMINAPI_VERSION__"', "`"$version`"" + $content | Set-Content $installerPath -NoNewline + + Write-Host "Successfully updated version in $installerPath" + } + else + { + throw "Could not find installer module at $installerPath" + } $installScriptPath = "Installer.AdminApi/install.ps1" - $content = Get-Content $installScriptPath -Raw - $content = $content -replace '"__ADMINAPI_VERSION__"', "`"$versionWithoutPrefix`"" - $content | Set-Content $installScriptPath -NoNewline - Write-Output "Updated version to $version (or $versionWithoutPrefix) in all installer" + if (Test-Path $installScriptPath) + { + $content = Get-Content $installScriptPath -Raw + $content = $content -replace '"__ADMINAPI_VERSION__"', "`"$version`"" + $content | Set-Content $installScriptPath -NoNewline + + Write-Host "Successfully updated version in $installScriptPath" + } + else + { + throw "Could not find installer script at $installScriptPath" + } + + # Verify the replacement + $verifyContent = Get-Content $installerPath -Raw + + if ($verifyContent -match '__ADMINAPI_VERSION__') + { + throw "Token replacement failed - __ADMINAPI_VERSION__ still found in $installerPath" + } + + $verifyInstallScriptContent = Get-Content $installScriptPath -Raw + + if ($verifyInstallScriptContent -match '__ADMINAPI_VERSION__') + { + throw "Token replacement failed - __ADMINAPI_VERSION__ still found in $installScriptPath" + } + Write-Host "Verification passed - token successfully replaced with version: $version" - name: Create NuGet Packages if: success() @@ -112,11 +161,10 @@ jobs: sbom-create: name: Create SBOM - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest needs: pack permissions: - actions: read - contents: write + actions: write outputs: sbom-hash-code: ${{ steps.sbom-hash-code.outputs.sbom-hash-code }} steps: @@ -167,7 +215,7 @@ jobs: sbom-attach: name: Attach SBOM file - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest needs: - sbom-create - pack @@ -229,7 +277,7 @@ jobs: publish-package: name: Publish NuGet Package needs: pack - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest defaults: run: shell: pwsh @@ -257,7 +305,7 @@ jobs: docker-publish: name: Publish to Docker Hub - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest needs: - publish-package steps: @@ -278,11 +326,12 @@ jobs: PACKAGEVERSION=${REF} fi - if [[ $PACKAGEVERSION =~ "alpha" ]] + if [[ $PACKAGEVERSION =~ "pre" ]] then # Pre-releases get the tag "pre" - APITAGS="${{ env.IMAGE_NAME }}:pre-1.3" - DBTAGS="${{ env.DATABASE_IMAGE_NAME }}:pre-1.3" + APITAGS="${{ env.IMAGE_NAME }}:pre" + DBTAGS="${{ env.DATABASE_IMAGE_NAME }}:pre" + DBTAGS6x="${{ env.DATABASE_IMAGE_NAME }}:pre-for-6.2" else # Releases get the version, plus shortened form for minor release. # We are not using shortened form for major or using "latest" @@ -295,17 +344,21 @@ jobs: SEMVERSION=${PACKAGEVERSION:1} # strip off the leading 'v' echo "APITAGS=$APITAGS" >> $GITHUB_OUTPUT echo "DBTAGS=$DBTAGS" >> $GITHUB_OUTPUT + echo "DBTAGS6x=$DBTAGS6x" >> $GITHUB_OUTPUT echo "VERSION=$SEMVERSION" >> $GITHUB_OUTPUT - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 - - name: Log in to Docker Hub uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_HUB_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 + - name: Extract metadata (tags, labels) for admin api image id: metaapi uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 @@ -316,9 +369,12 @@ jobs: uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0 with: context: "{{defaultContext}}:Docker" - cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:pre-1.3 + cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:pre cache-to: type=inline - build-args: VERSION=${{ steps.prepare-tags.outputs.VERSION }} + platforms: linux/amd64,linux/arm64 + build-args: | + VERSION=${{ steps.prepare-tags.outputs.VERSION }} + ADMIN_API_VERSION=${{ steps.prepare-tags.outputs.VERSION }} file: api.pgsql.Dockerfile tags: ${{ steps.prepare-tags.outputs.APITAGS }} labels: ${{ steps.metaapi.outputs.labels }} @@ -330,14 +386,28 @@ jobs: with: images: ${{ env.DATABASE_IMAGE_NAME }} - - name: Build and push admin api database image + - name: Build and push admin api database image 7.x uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0 with: - context: "{{defaultContext}}:Docker/Settings/DB-Admin/pgsql" - cache-from: type=registry,ref=${{ env.DATABASE_IMAGE_NAME }}:pre-1.3 + context: "{{defaultContext}}:Docker/Settings/V2/DB-Admin/pgsql" + cache-from: type=registry,ref=${{ env.DATABASE_IMAGE_NAME }}:pre cache-to: type=inline - build-args: VERSION=${{ steps.prepare-tags.outputs.VERSION }} + platforms: linux/amd64,linux/arm64 + build-args: ADMIN_API_VERSION=${{ steps.prepare-tags.outputs.VERSION }} file: Dockerfile tags: ${{ steps.prepare-tags.outputs.DBTAGS }} labels: ${{ steps.metadatabase.outputs.labels }} push: true + + - name: Build and push admin api database image 6.x + uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0 + if: startsWith(github.event.release.tag_name, 'Pre-Release') || startsWith(github.event.release.tag_name, 'v1.') + with: + context: "{{defaultContext}}:Docker/Settings/V1/DB-Admin/pgsql" + cache-from: type=registry,ref=${{ env.DATABASE_IMAGE_NAME }}:pre-for-6.2 + cache-to: type=inline + build-args: ADMIN_API_VERSION=${{ steps.prepare-tags.outputs.VERSION }} + file: Dockerfile + tags: ${{ steps.prepare-tags.outputs.DBTAGS6x }} + labels: ${{ steps.metadatabase.outputs.labels }} + push: true diff --git a/.github/workflows/on-pullrequest-dockerfile.yml b/.github/workflows/on-pullrequest-dockerfile.yml index 5802bb7bf..295e1671d 100644 --- a/.github/workflows/on-pullrequest-dockerfile.yml +++ b/.github/workflows/on-pullrequest-dockerfile.yml @@ -34,10 +34,13 @@ jobs: matrix: dockerfile: [ - { name: "api-database", path: "Docker/Settings/DB-Admin/pgsql/Dockerfile", type: "published" }, - { name: "postgres", path: "Docker/api.mssql.Dockerfile", type: "published" }, - { name: "database", path: "Docker/db.pgsql.admin.Dockerfile", type: "local" }, - { name: "database", path: "Docker/db.mssql.admin.Dockerfile", type: "local" }, + { name: "v2-api-database", path: "Docker/Settings/V2/DB-Admin/pgsql/Dockerfile", type: "published" }, + { name: "v1-api-database", path: "Docker/Settings/V1/DB-Admin/pgsql/Dockerfile", type: "published" }, + { name: "postgres", path: "Docker/api.pgsql.Dockerfile", type: "published" }, + { name: "v2-gateway", path: "Docker/Settings/V2/gateway/Dockerfile", type: "local" }, + { name: "v1-gateway", path: "Docker/Settings/V1/gateway/Dockerfile", type: "local" }, + { name: "v2-database", path: "Docker/V2/db.pgsql.admin.Dockerfile", type: "local" }, + { name: "v1-database", path: "Docker/V1/db.pgsql.admin.Dockerfile", type: "local" }, { name: "development", path: "Docker/dev.pgsql.Dockerfile", type: "local" }, ] steps: @@ -51,8 +54,8 @@ jobs: FEED="9f7770ac-66d9-4fbc-b81e-b5ad79002b62" PACKAGE="db5612a0-336b-4960-9092-f3be0b63d13e" - VERSIONS=$(curl https://feeds.dev.azure.com/ed-fi-alliance/$FEED/_apis/Packaging/Feeds/EdFi/Packages/$PACKAGE?includeAllVersions=true) - LATEST=$(echo $VERSIONS | jq '[.versions[] | select(.views[].name == "Release") | .version | select(startswith("1."))][0]') + VERSIONS=$(curl https://feeds.dev.azure.com/ed-fi-alliance/$FEED/_apis/Packaging/Feeds/EdFi/Packages/$PACKAGE) + LATEST=$(echo $VERSIONS | jq '.versions[] | select(.isLatest == true) | .version') echo "latest version: $LATEST" echo "VERSION=$LATEST" >> $GITHUB_OUTPUT @@ -75,16 +78,20 @@ jobs: cd $folder dockerfile=$(echo ${{matrix.dockerfile.path}} | awk -F"/" '{print $NF}') - docker build -f $dockerfile -t ${{ matrix.dockerfile.name }} --build-context assets=.. --build-arg="VERSION=${{ steps.versions.outputs.VERSION }}" . - + lower_path=$(echo "${{ matrix.dockerfile.path }}" | tr '[:upper:]' '[:lower:]') + if [[ "$lower_path" == *v2* || "$lower_path" == *v1* ]] + then + docker build -f $dockerfile -t ${{ matrix.dockerfile.name }} --build-context assets=../.. --build-arg="ADMIN_API_VERSION=${{ steps.versions.outputs.VERSION }}" . + else + docker build -f $dockerfile -t ${{ matrix.dockerfile.name }} --build-context assets=.. --build-arg="ADMIN_API_VERSION=${{ steps.versions.outputs.VERSION }}" . + fi + - name: Analyze - uses: docker/scout-action@b23590dc1e4d09febc00cfcbc51e9e8c0f7ee9f3 # v1.16.1 + uses: docker/scout-action@67eb1afe777307506aaecb9acd9a0e0389cb99ae # v1.5.0 with: command: cves image: local://${{ matrix.dockerfile.name }} - sarif-file: sarif-${{ matrix.dockerfile.name }}-${{ strategy.job-index }}.output.json - only-severities: critical,high - only-fixed: true + sarif-file: sarif-${{ matrix.dockerfile.name }}.output.json summary: true - name: Upload SARIF result @@ -92,4 +99,4 @@ jobs: if: ${{ github.event_name != 'pull_request_target' }} uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 #codeql-bundle-v3.28.0 with: - sarif_file: sarif-${{ matrix.dockerfile.name }}-${{ strategy.job-index }}.output.json + sarif_file: sarif-${{ matrix.dockerfile.name }}.output.json diff --git a/.github/workflows/on-pullrequest.yml b/.github/workflows/on-pullrequest.yml index 6b457adcf..76542f6cf 100644 --- a/.github/workflows/on-pullrequest.yml +++ b/.github/workflows/on-pullrequest.yml @@ -11,6 +11,11 @@ on: - main - "*-hotfix" paths: + # TODO: restore this with AA-1601, except run in a PS-specific + # workflow, so that the C# build and CodeQL do not run + # unnecessarily. + # - "**/*.ps1" + # - "**/*.psm1" - "**/*.cs" - "**/*.csproj" - ".github/**/*.yml" @@ -19,6 +24,10 @@ on: permissions: read-all jobs: + # TODO: restore this with AA-1601 + # run-ps-lint: + # name: PowerShell Linter + # uses: Ed-Fi-Alliance-OSS/Ed-Fi-Actions/.github/workflows/powershell-analyzer.yml@main scan-actions-bidi: name: Scan Actions, scan all files for BIDI Trojan Attacks uses: ed-fi-alliance-oss/ed-fi-actions/.github/workflows/repository-scanner.yml@main @@ -38,12 +47,25 @@ jobs: - name: Build run: ./build.ps1 -Command Build -Configuration Debug + - name: Install Coverlet Tools + if: success() + run: | + dotnet tool install --global coverlet.console + dotnet tool install --global dotnet-reportgenerator-globaltool + - name: Run Unit Tests if: success() - run: ./build.ps1 -Command UnitTest -Configuration Debug + run: ./build.ps1 -Command UnitTest -Configuration Debug -RunCoverageAnalysis + + - name: Upload Coverage Report + if: always() + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5 + with: + name: Coverage Report + path: coveragereport - name: Upload Test Results - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5 with: name: csharp-tests path: "**/*.trx" @@ -69,12 +91,50 @@ jobs: if: success() uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # codeql-bundle-v3.28.0 + run-integration-tests: + name: Run integration tests + env: + DB_Password: P@55w0rd + runs-on: ubuntu-22.04 + defaults: + run: + shell: pwsh + services: + sqlserver: + image: mcr.microsoft.com/mssql/server@sha256:d7f2c670f0cd807b4dc466b8887bd2b39a4561f624c154896f5564ea38efd13a + env: + ACCEPT_EULA: "Y" + SA_PASSWORD: ${{ env.DB_Password }} + MSSQL_ENABLE_HADR: "1" + ports: + - 1433:1433 + steps: + - name: Checkout the Repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Setup Nuget.exe + uses: nuget/setup-nuget@323ab0502cd38fdc493335025a96c8fdb0edc71f #v2.0.1 + + - name: Build + run: ./build.ps1 -Command Build -Configuration Debug + + - name: Run Integration Tests + if: success() + run: ./build.ps1 -Command IntegrationTest -Configuration Debug -UseIntegratedSecurity:$false -DbUsername "sa" -DbPassword ${{ env.DB_Password }} + + - name: Upload Test Results + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5 + with: + name: csharp-integration-tests + path: "**/*.trx" + retention-days: 5 + event_file: name: "Event File" runs-on: ubuntu-latest steps: - name: Upload - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5 with: name: Event File path: ${{ github.event_path }} diff --git a/.github/workflows/on-release.yml b/.github/workflows/on-release.yml index 78fc14318..d76e2db0e 100644 --- a/.github/workflows/on-release.yml +++ b/.github/workflows/on-release.yml @@ -9,14 +9,15 @@ on: types: - released +permissions: read-all + env: ARTIFACTS_API_KEY: ${{ secrets.AZURE_ARTIFACTS_PERSONAL_ACCESS_TOKEN }} + ARTIFACTS_FEED_URL: "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_packaging/EdFi/nuget/v3/index.json" ARTIFACTS_PACKAGES_URL: "https://pkgs.dev.azure.com/ed-fi-alliance/9f7770ac-66d9-4fbc-b81e-b5ad79002b62/_apis/packaging/feeds/338858dc-b976-4355-ab70-5e713b1f56be/nuget/packagesBatch?api-version=7.1-preview.1" RELEASE_VIEW_ID: "53acfbeb-77f2-4ef6-8596-dc19e5802775" #Release ARTIFACTS_USERNAME: ${{ secrets.AZURE_ARTIFACTS_USER_NAME }} -permissions: read-all - jobs: delete-pre-releases: name: Delete Unnecessary Pre-Releases @@ -60,6 +61,8 @@ jobs: promote-Azure-artifact: name: Promote Azure Artifact runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout the repo uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -71,8 +74,8 @@ jobs: PackagesURL = "${{ env.ARTIFACTS_PACKAGES_URL }}" Username = "${{ env.ARTIFACTS_USERNAME }}" ViewId = "${{ env.RELEASE_VIEW_ID }}" - Password = (ConvertTo-SecureString -String "${{ env.ARTIFACTS_API_KEY }}" -AsPlainText -Force) ReleaseRef = "${{ github.ref_name }}" + Password = (ConvertTo-SecureString -String "${{ env.ARTIFACTS_API_KEY }}" -AsPlainText -Force) } Import-Module ./eng/promote-packages.psm1 diff --git a/.github/workflows/openapi-md.yml b/.github/workflows/openapi-md.yml new file mode 100644 index 000000000..7bdb15e44 --- /dev/null +++ b/.github/workflows/openapi-md.yml @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +name: Create PR to update doc and openapi definition + +on: + workflow_dispatch: + inputs: + version: + description: 'Version Name. Example -> 2.2.2 will result "admin-api-2.2.2.yaml" "admin-api-2.2.2-summary.md"' + required: true + type: string +permissions: read-all + +env: + CI_COMMIT_MESSAGE: Add YAML and markdown file api-specification version ${{ inputs.version }} + CI_COMMIT_AUTHOR: github-actions[bot] + CI_COMMIT_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + BRANCH_NAME: openapi-${{ inputs.version }} + +jobs: + create-doc-and-openapiyaml: + name: Generate documentation + runs-on: ubuntu-latest + permissions: + contents: write + defaults: + run: + shell: pwsh + steps: + - name: Checkout the Repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Git create branch + run: | + git checkout -b ${{ env.BRANCH_NAME }} + git push --set-upstream origin ${{ env.BRANCH_NAME }} + + - name: Install Swashbuckle CLI + run: dotnet tool install Swashbuckle.AspNetCore.Cli --version 6.6.2 --create-manifest-if-needed + + - name: Install widdershins CLI + run: npm install -g widdershins + + - name: Build and generate YAML and MD files + run: | + $p = @{ + Authority = "http://api" + IssuerUrl = "https://localhost" + DatabaseEngine = "PostgreSql" + PathBase = "adminapi" + SigningKey = "test" + AdminDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Admin;Application Name=EdFi.Ods.AdminApi;" + SecurityDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Security;Application Name=EdFi.Ods.AdminApi;" + } + ./build.ps1 -APIVersion ${{ inputs.version }} -Configuration Release -DockerEnvValues $p -Command GenerateOpenAPIAndMD + + - name: Git add files + run: | + git add docs/api-specifications/openapi-yaml/* + git add docs/api-specifications/markdown/* + git restore Application/EdFi.Ods.AdminApi/appsettings.json + git status --porcelain + + - name: Commit file + id: commit + uses: planetscale/ghcommit-action@d4176bfacef926cc2db351eab20398dfc2f593b5 #v0.2.0 + with: + commit_message: "${{ env.CI_COMMIT_MESSAGE }}" + repo: ${{ github.repository }} + branch: ${{ env.BRANCH_NAME }} + file_pattern: '*.yaml *.md' + + - name: Create PR + run: gh pr create -B main -H ${{ env.BRANCH_NAME }} --title '[Github Action] Open API documentation version ${{ inputs.version }}' --body 'Created by Github action' diff --git a/.gitignore b/.gitignore index e3dfc902e..d570fb468 100644 --- a/.gitignore +++ b/.gitignore @@ -62,5 +62,13 @@ credentials-*.xml libs/ libs.codegen/ -Docker/Application -dhparam.pem +#Docker +Docker/Application/ +Docker/Settings/ssl/server.crt +Docker/Settings/ssl/server.key +Docker/Settings/ssl/dhparam.pem + +# Code coverage +coverage +coverage.* +coveragereport/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1a2f1c89e..9e2955bea 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,6 +4,10 @@ "EditorConfig.EditorConfig", "ms-dotnettools.csharp", "ms-vscode.powershell", - "streetsidesoftware.code-spell-checker" + "csharpier.csharpier-vscode", + "humao.rest-client", + "streetsidesoftware.code-spell-checker", + "bierner.markdown-mermaid", + "DavidAnson.vscode-markdownlint" ] -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 348747261..778810af9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,10 +1,14 @@ { + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", "configurations": [ { - "name": "C#: EdFi.Ods.AdminApi [Default Configuration]", + "name": "C#: Admin API 2 Debug", "type": "dotnet", "request": "launch", - "projectPath": "${workspaceFolder}\\Application\\EdFi.Ods.AdminApi\\EdFi.Ods.AdminApi.csproj" + "projectPath": "${workspaceFolder}/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj" } ] -} + diff --git a/.vscode/settings.json b/.vscode/settings.json index e18f2117c..8721e71f7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,7 +18,7 @@ ] } ], - "psi-header.lang-config":[ + "psi-header.lang-config": [ { "_": "Apply double slash-based comments", "language": "csharp", @@ -67,8 +67,31 @@ ], "psi-header.changes-tracking": { "autoHeader": "autoSave", - "exclude": [ - "css", "json", "xml", "config", "plaintext", "markdown", "batch" - ] - } -} \ No newline at end of file + "exclude": ["css", "json", "xml", "config", "plaintext", "markdown", "batch"] + }, + "dotnet.defaultSolution": "Application/Ed-Fi-ODS-AdminApi.sln", + "markdownlint.config": { + "default": true, + "line-length": false, + "no-inline-html": { + "allowed_elements": ["kbd", "details", "summary", "br", "iframe", "sup"] + }, + "emphasis-style": { + "style": "underscore" + }, + "ul-style": { + "style": "asterisk" + } + }, + "[markdown]": { + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.defaultFormatter": "DavidAnson.vscode-markdownlint" + }, + "editor.codeActionsOnSave": { + "source.fixAll.markdownlint": "explicit" + }, + "cSpell.words": [ + "Shouldly" + ] +} diff --git a/Application/Directory.Packages.props b/Application/Directory.Packages.props new file mode 100644 index 000000000..bb56bd234 --- /dev/null +++ b/Application/Directory.Packages.props @@ -0,0 +1,101 @@ + + + true + + + $(NoWarn);NU1507;NU1604;NU1602;NU1603;NU1701;NU1902;NU1903 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Application/Ed-Fi-ODS-AdminApi.sln b/Application/Ed-Fi-ODS-AdminApi.sln index 82ca90a56..b1bc7f739 100644 --- a/Application/Ed-Fi-ODS-AdminApi.sln +++ b/Application/Ed-Fi-ODS-AdminApi.sln @@ -1,65 +1,108 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.33516.290 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1D317139-CB19-4D7F-91AC-9A560189C55B}" - ProjectSection(SolutionItems) = preProject - ..\CONTRIBUTORS.md = ..\CONTRIBUTORS.md - ..\NOTICES.md = ..\NOTICES.md - ..\README.md = ..\README.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTests", "UnitTests", "{9A9D18B4-718D-4681-BAFE-A1C42E18A7CC}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi.UnitTests", "EdFi.Ods.AdminApi.UnitTests\EdFi.Ods.AdminApi.UnitTests.csproj", "{F62C9CF6-A632-4894-B61A-674198DAB86E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi", "EdFi.Ods.AdminApi\EdFi.Ods.AdminApi.csproj", "{FBFFA828-316C-43C8-B52A-4D64C51DF6EB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IntegrationTests", "IntegrationTests", "{D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi.DBTests", "EdFi.Ods.AdminApi.DBTests\EdFi.Ods.AdminApi.DBTests.csproj", "{73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|x64.ActiveCfg = Debug|Any CPU - {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|x64.Build.0 = Debug|Any CPU - {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|Any CPU.Build.0 = Release|Any CPU - {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|x64.ActiveCfg = Release|Any CPU - {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|x64.Build.0 = Release|Any CPU - {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|x64.ActiveCfg = Debug|Any CPU - {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|x64.Build.0 = Debug|Any CPU - {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|Any CPU.Build.0 = Release|Any CPU - {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|x64.ActiveCfg = Release|Any CPU - {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|x64.Build.0 = Release|Any CPU - {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|x64.ActiveCfg = Debug|Any CPU - {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|x64.Build.0 = Debug|Any CPU - {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|Any CPU.Build.0 = Release|Any CPU - {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|x64.ActiveCfg = Release|Any CPU - {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|x64.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {F62C9CF6-A632-4894-B61A-674198DAB86E} = {9A9D18B4-718D-4681-BAFE-A1C42E18A7CC} - {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5} = {D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {30CF2BE4-58CA-4598-9B59-D334FC971A0F} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33516.290 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1D317139-CB19-4D7F-91AC-9A560189C55B}" + ProjectSection(SolutionItems) = preProject + ..\CONTRIBUTORS.md = ..\CONTRIBUTORS.md + ..\NOTICES.md = ..\NOTICES.md + ..\README.md = ..\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTests", "UnitTests", "{9A9D18B4-718D-4681-BAFE-A1C42E18A7CC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi.UnitTests", "EdFi.Ods.AdminApi.UnitTests\EdFi.Ods.AdminApi.UnitTests.csproj", "{F62C9CF6-A632-4894-B61A-674198DAB86E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi", "EdFi.Ods.AdminApi\EdFi.Ods.AdminApi.csproj", "{FBFFA828-316C-43C8-B52A-4D64C51DF6EB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IntegrationTests", "IntegrationTests", "{D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi.DBTests", "EdFi.Ods.AdminApi.DBTests\EdFi.Ods.AdminApi.DBTests.csproj", "{73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.Common", "EdFi.Ods.AdminApi.Common\EdFi.Ods.AdminApi.Common.csproj", "{C9C86866-562B-4EA3-9AAC-F3297F0754D6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.Common.UnitTests", "EdFi.Ods.AdminApi.Common.UnitTests\EdFi.Ods.AdminApi.Common.UnitTests.csproj", "{D0B566A2-000E-48FC-9CD0-702F7A00CA76}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.V1", "EdFi.Ods.AdminApi.V1\EdFi.Ods.AdminApi.V1.csproj", "{760F71C6-9FD9-4F5B-AE77-34501EE76FBD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.V1.DBTests", "EdFi.Ods.AdminApi.V1.DBTests\EdFi.Ods.AdminApi.V1.DBTests.csproj", "{4377A3CF-71EC-4097-AC8C-789ED63075C9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|x64.ActiveCfg = Debug|Any CPU + {F62C9CF6-A632-4894-B61A-674198DAB86E}.Debug|x64.Build.0 = Debug|Any CPU + {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|Any CPU.Build.0 = Release|Any CPU + {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|x64.ActiveCfg = Release|Any CPU + {F62C9CF6-A632-4894-B61A-674198DAB86E}.Release|x64.Build.0 = Release|Any CPU + {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|x64.ActiveCfg = Debug|Any CPU + {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Debug|x64.Build.0 = Debug|Any CPU + {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|Any CPU.Build.0 = Release|Any CPU + {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|x64.ActiveCfg = Release|Any CPU + {FBFFA828-316C-43C8-B52A-4D64C51DF6EB}.Release|x64.Build.0 = Release|Any CPU + {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|x64.ActiveCfg = Debug|Any CPU + {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Debug|x64.Build.0 = Debug|Any CPU + {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|Any CPU.Build.0 = Release|Any CPU + {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|x64.ActiveCfg = Release|Any CPU + {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|x64.Build.0 = Release|Any CPU + {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Debug|x64.Build.0 = Debug|Any CPU + {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Release|Any CPU.Build.0 = Release|Any CPU + {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Release|x64.ActiveCfg = Release|Any CPU + {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Release|x64.Build.0 = Release|Any CPU + {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Debug|x64.Build.0 = Debug|Any CPU + {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Release|Any CPU.Build.0 = Release|Any CPU + {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Release|x64.ActiveCfg = Release|Any CPU + {D0B566A2-000E-48FC-9CD0-702F7A00CA76}.Release|x64.Build.0 = Release|Any CPU + {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Debug|x64.ActiveCfg = Debug|Any CPU + {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Debug|x64.Build.0 = Debug|Any CPU + {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Release|Any CPU.Build.0 = Release|Any CPU + {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Release|x64.ActiveCfg = Release|Any CPU + {760F71C6-9FD9-4F5B-AE77-34501EE76FBD}.Release|x64.Build.0 = Release|Any CPU + {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Debug|x64.ActiveCfg = Debug|Any CPU + {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Debug|x64.Build.0 = Debug|Any CPU + {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Release|Any CPU.Build.0 = Release|Any CPU + {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Release|x64.ActiveCfg = Release|Any CPU + {4377A3CF-71EC-4097-AC8C-789ED63075C9}.Release|x64.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1D317139-CB19-4D7F-91AC-9A560189C55B} = {D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6} + {F62C9CF6-A632-4894-B61A-674198DAB86E} = {9A9D18B4-718D-4681-BAFE-A1C42E18A7CC} + {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5} = {D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6} + {4377A3CF-71EC-4097-AC8C-789ED63075C9} = {D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {30CF2BE4-58CA-4598-9B59-D334FC971A0F} + EndGlobalSection +EndGlobal diff --git a/Application/EdFi.Ods.AdminApi.Common.UnitTests/EdFi.Ods.AdminApi.Common.UnitTests.csproj b/Application/EdFi.Ods.AdminApi.Common.UnitTests/EdFi.Ods.AdminApi.Common.UnitTests.csproj new file mode 100644 index 000000000..5d29f9027 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common.UnitTests/EdFi.Ods.AdminApi.Common.UnitTests.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + enable + enable + + false + true + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/Application/EdFi.Ods.AdminApi.Common.UnitTests/Features/AdminApiErrorTest.cs b/Application/EdFi.Ods.AdminApi.Common.UnitTests/Features/AdminApiErrorTest.cs new file mode 100644 index 000000000..2c1e7b748 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common.UnitTests/Features/AdminApiErrorTest.cs @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Shouldly; +using Results = FluentValidation.Results; + +namespace EdFi.Ods.AdminApi.Common.UnitTests.Features; + +[TestFixture] +public class AdminApiErrorTest +{ + [Test] + public void Validation_ShouldReturnValidationProblem() + { + // Arrange + var validationFailures = new List + { + new("Property1", "Error1"), + new("Property1", "Error2"), + new("Property2", "Error3") + }; + + // Act + var result = AdminApiError.Validation(validationFailures); + + // Assert + result.ShouldBeOfType(); + } + + [Test] + public void Validation_ShouldNotBeNull() + { + // Arrange + var validationFailures = new List + { + new("Property1", "Error1"), + new("Property1", "Error2"), + new("Property2", "Error3") + }; + + // Act + var result = AdminApiError.Validation(validationFailures); + + // Assert + var validationProblemDetails = result as ProblemHttpResult; + validationProblemDetails.ShouldNotBeNull(); + } + + [Test] + public void Validation_ShouldContainErrors() + { + // Arrange + var validationFailures = new List + { + new("Property1", "Error1"), + new("Property1", "Error2"), + new("Property2", "Error3") + }; + + // Act + var result = AdminApiError.Validation(validationFailures); + + // Assert + var details = (result as ProblemHttpResult)?.ProblemDetails as HttpValidationProblemDetails; + details.ShouldNotBeNull(); + details.Errors.ShouldContainKey("Property1"); + details.Errors["Property1"].ShouldBeEquivalentTo(new[] { "Error1", "Error2" }); + details.Errors.ShouldContainKey("Property2"); + details.Errors["Property2"].ShouldBeEquivalentTo(new[] { "Error3" }); + } + + [Test] + public void Unexpected_ShouldReturnProblemWithMessage() + { + // Arrange + var message = "Unexpected error occurred"; + + // Act + var result = AdminApiError.Unexpected(message); + + // Assert + result.ShouldBeOfType(); + } + + [Test] + public void Unexpected_ShouldNotBeNull() + { + // Arrange + var message = "Unexpected error occurred"; + + // Act + var result = AdminApiError.Unexpected(message); + + // Assert + var problemDetails = result as ProblemHttpResult; + problemDetails.ShouldNotBeNull(); + } + + [Test] + public void Unexpected_ShouldHaveCorrectTitle() + { + // Arrange + var message = "Unexpected error occurred"; + + // Act + var result = AdminApiError.Unexpected(message); + + // Assert + var problemDetails = result as ProblemHttpResult; + problemDetails?.ProblemDetails.Title.ShouldBe(message); + } + + [Test] + public void Unexpected_ShouldHaveCorrectStatus() + { + // Arrange + var message = "Unexpected error occurred"; + + // Act + var result = AdminApiError.Unexpected(message); + + // Assert + var problemDetails = result as ProblemHttpResult; + problemDetails?.ProblemDetails.Status.ShouldBe(500); + } + + [Test] + public void Unexpected_WithErrors_ShouldReturnProblemWithMessageAndErrors() + { + // Arrange + var message = "Unexpected error occurred"; + var errors = new[] { "Error1", "Error2" }; + + // Act + var result = AdminApiError.Unexpected(message, errors); + + // Assert + result.ShouldBeOfType(); + } + + [Test] + public void Unexpected_WithErrors_ShouldNotBeNull() + { + // Arrange + var message = "Unexpected error occurred"; + var errors = new[] { "Error1", "Error2" }; + + // Act + var result = AdminApiError.Unexpected(message, errors); + + // Assert + var problemDetails = result as ProblemHttpResult; + problemDetails.ShouldNotBeNull(); + } + + [Test] + public void Unexpected_WithErrors_ShouldHaveCorrectTitle() + { + // Arrange + var message = "Unexpected error occurred"; + var errors = new[] { "Error1", "Error2" }; + + // Act + var result = AdminApiError.Unexpected(message, errors); + + // Assert + var problemDetails = result as ProblemHttpResult; + problemDetails?.ProblemDetails.Title.ShouldBe(message); + } + + [Test] + public void Unexpected_WithErrors_ShouldHaveCorrectStatus() + { + // Arrange + var message = "Unexpected error occurred"; + var errors = new[] { "Error1", "Error2" }; + + // Act + var result = AdminApiError.Unexpected(message, errors); + + // Assert + var problemDetails = result as ProblemHttpResult; + problemDetails?.ProblemDetails.Status.ShouldBe(500); + } + + [Test] + public void Unexpected_WithErrors_ShouldHaveCorrectExtensions() + { + // Arrange + var message = "Unexpected error occurred"; + var errors = new[] { "Error1", "Error2" }; + + // Act + var result = AdminApiError.Unexpected(message, errors); + + // Assert + var problemDetails = result as ProblemHttpResult; + problemDetails?.ProblemDetails.Extensions["errors"].ShouldBeEquivalentTo(errors); + } + + [Test] + public void Unexpected_WithException_ShouldReturnProblemWithExceptionMessage() + { + // Arrange + var exception = new Exception("Exception message"); + + // Act + var result = AdminApiError.Unexpected(exception); + + // Assert + result.ShouldBeOfType(); + } + + [Test] + public void Unexpected_WithException_ShouldNotBeNull() + { + // Arrange + var exception = new Exception("Exception message"); + + // Act + var result = AdminApiError.Unexpected(exception); + + // Assert + var problemDetails = result as ProblemHttpResult; + problemDetails.ShouldNotBeNull(); + } + + [Test] + public void Unexpected_WithException_ShouldHaveCorrectTitle() + { + // Arrange + var exception = new Exception("Exception message"); + + // Act + var result = AdminApiError.Unexpected(exception); + + // Assert + var problemDetails = result as ProblemHttpResult; + problemDetails?.ProblemDetails.Title.ShouldBe(exception.Message); + } + + [Test] + public void Unexpected_WithException_ShouldHaveCorrectStatus() + { + // Arrange + var exception = new Exception("Exception message"); + + // Act + var result = AdminApiError.Unexpected(exception); + + // Assert + var problemDetails = result as ProblemHttpResult; + problemDetails?.ProblemDetails.Status.ShouldBe(500); + } + + [Test] + public void NotFound_ShouldReturnNotFoundWithResourceNameAndId() + { + // Arrange + var resourceName = "Resource"; + var id = 123; + + // Act + var result = AdminApiError.NotFound(resourceName, id); + + // Assert + result.ShouldBeOfType>(); + } + + [Test] + public void NotFound_ShouldNotBeNull() + { + // Arrange + var resourceName = "Resource"; + var id = 123; + + // Act + var result = AdminApiError.NotFound(resourceName, id); + + // Assert + var notFoundResult = result as NotFound; + notFoundResult.ShouldNotBeNull(); + } + + [Test] + public void NotFound_ShouldHaveCorrectValue() + { + // Arrange + var resourceName = "Resource"; + var id = 123; + + // Act + var result = AdminApiError.NotFound(resourceName, id); + + // Assert + var notFoundResult = result as NotFound; + notFoundResult?.Value.ShouldBe($"Not found: {resourceName} with ID {id}"); + } + + [Test] + public void Validation_WithEmptyErrors_ShouldReturnValidationProblem() + { + // Act + var result = AdminApiError.Validation(new List()); + + // Assert + result.ShouldBeOfType(); + } + + [Test] + public void Validation_WithEmptyErrors_ShouldNotBeNull() + { + // Act + var result = AdminApiError.Validation(new List()); + + // Assert + var validationProblemDetails = result as ProblemHttpResult; + validationProblemDetails.ShouldNotBeNull(); + } + + [Test] + public void Validation_WithEmptyErrors_ShouldHaveEmptyErrors() + { + // Act + var result = AdminApiError.Validation(new List()); + + // Assert + var details = (result as ProblemHttpResult)?.ProblemDetails as HttpValidationProblemDetails; + details.ShouldNotBeNull(); + details.Errors.ShouldBeEmpty(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs b/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs new file mode 100644 index 000000000..36c03fb1a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Constants; + +public class Constants +{ + public const string TenantsCacheKey = "tenants"; + public const string TenantNameDescription = "Admin API Tenant Name"; + public const string TenantConnectionStringDescription = "Tenant connection strings"; + public const string DefaultTenantName = "default"; +} + +public enum AdminApiMode +{ + V2, + V1, + Unversioned +} diff --git a/Application/EdFi.Ods.AdminApi.Common/EdFi.Ods.AdminApi.Common.csproj b/Application/EdFi.Ods.AdminApi.Common/EdFi.Ods.AdminApi.Common.csproj new file mode 100644 index 000000000..2c1d0c024 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/EdFi.Ods.AdminApi.Common.csproj @@ -0,0 +1,37 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi.Common/Features/AdminApiError.cs b/Application/EdFi.Ods.AdminApi.Common/Features/AdminApiError.cs new file mode 100644 index 000000000..798c97ede --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Features/AdminApiError.cs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using FluentValidation.Results; +using Microsoft.AspNetCore.Http; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Common.Features; + +[SwaggerSchema(Title = "AdminApiError", Description = "Wrapper schema for all error responses")] +public static class AdminApiError +{ + public static IResult Validation(IEnumerable errors) => + Results.ValidationProblem( + errors + .GroupBy(e => e.PropertyName) + .ToDictionary(g => g.Key, g => g.Select(f => f.ErrorMessage).ToArray()) + ); + + public static IResult Unexpected(string message) => Results.Problem(statusCode: 500, title: message); + + public static IResult Unexpected(string message, IEnumerable errors) => + Results.Problem( + statusCode: 500, + title: message, + extensions: new Dictionary { { "errors", errors } } + ); + + public static IResult Unexpected(Exception exception) => + Results.Problem(statusCode: 500, title: exception.Message); + + public static IResult NotFound(string resourceName, T id) => + Results.NotFound($"Not found: {resourceName} with ID {id}"); +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Features/FeatureCommonConstants.cs b/Application/EdFi.Ods.AdminApi.Common/Features/FeatureCommonConstants.cs new file mode 100644 index 000000000..441beff47 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Features/FeatureCommonConstants.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Features; + +public static class FeatureCommonConstants +{ + public const string DeletedSuccessResponseDescription = "Resource was successfully deleted."; + public const string InternalServerErrorResponseDescription = "Internal server error. An unhandled error occurred on the server. See the response body for details."; + public const string BadRequestResponseDescription = "Bad Request. The request was invalid and cannot be completed. See the response body for details."; +} diff --git a/Application/EdFi.Ods.AdminApi/Features/IFeature.cs b/Application/EdFi.Ods.AdminApi.Common/Features/IFeature.cs similarity index 81% rename from Application/EdFi.Ods.AdminApi/Features/IFeature.cs rename to Application/EdFi.Ods.AdminApi.Common/Features/IFeature.cs index 7f10c4402..52e42d6a6 100644 --- a/Application/EdFi.Ods.AdminApi/Features/IFeature.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Features/IFeature.cs @@ -3,7 +3,9 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -namespace EdFi.Ods.AdminApi.Features; +using Microsoft.AspNetCore.Routing; + +namespace EdFi.Ods.AdminApi.Common.Features; public interface IFeature { diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiEndpointBuilder.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiEndpointBuilder.cs new file mode 100644 index 000000000..3b17abaa4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiEndpointBuilder.cs @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Data; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Common.Infrastructure.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure; + +public class AdminApiEndpointBuilder +{ + private AdminApiEndpointBuilder(IEndpointRouteBuilder endpoints, + HttpVerb verb, string route, Delegate handler) + { + _endpoints = endpoints; + _verb = verb; + _route = route.Trim('/'); + _handler = handler; + _pluralResourceName = _route.Split('/')[0]; + } + + private readonly IEndpointRouteBuilder _endpoints; + private readonly HttpVerb? _verb; + private readonly string _route; + private readonly Delegate? _handler; + private readonly List> _routeOptions = []; + private readonly string _pluralResourceName; + private bool _allowAnonymous = false; + private IEnumerable _authorizationPolicies = []; + + public static AdminApiEndpointBuilder MapGet(IEndpointRouteBuilder endpoints, string route, Delegate handler) + => new(endpoints, HttpVerb.GET, route, handler); + + public static AdminApiEndpointBuilder MapPost(IEndpointRouteBuilder endpoints, string route, Delegate handler) + => new(endpoints, HttpVerb.POST, route, handler); + + public static AdminApiEndpointBuilder MapPut(IEndpointRouteBuilder endpoints, string route, Delegate handler) + => new(endpoints, HttpVerb.PUT, route, handler); + + public static AdminApiEndpointBuilder MapDelete(IEndpointRouteBuilder endpoints, string route, Delegate handler) + => new(endpoints, HttpVerb.DELETE, route, handler); + + /// + /// Includes the specified authorization policy in the endpoint. + /// + /// List of authorization Policies to validate + /// + public AdminApiEndpointBuilder RequireAuthorization(IEnumerable authorizationPolicies) + { + _authorizationPolicies = authorizationPolicies.Select(policy => policy.PolicyName).ToList(); + return this; + } + + /// + /// Includes the specified authorization policy in the endpoint. + /// + /// List of authorization Policies to validate + /// + public AdminApiEndpointBuilder RequireAuthorization(PolicyDefinition authorizationPolicies) + { + _authorizationPolicies = [authorizationPolicies.PolicyName]; + return this; + } + + public void BuildForVersions(params AdminApiVersions.AdminApiVersion[] versions) + { + BuildForVersions(string.Empty, false, versions); + } + + public void BuildForVersions(string authorizationPolicy, bool display409 = false, params AdminApiVersions.AdminApiVersion[] versions) + { + if (versions.Length == 0) + throw new ArgumentException("Must register for at least 1 version"); + if (_route == null) + throw new InvalidOperationException("Invalid endpoint registration. Route must be specified"); + if (_handler == null) + throw new InvalidOperationException("Invalid endpoint registration. Handler must be specified"); + + foreach (var version in versions) + { + if (version == null) + throw new ArgumentException("Version cannot be null"); + + var versionedRoute = $"/{version}/{_route}"; + + var builder = _verb switch + { + HttpVerb.GET => _endpoints.MapGet(versionedRoute, _handler), + HttpVerb.POST => _endpoints.MapPost(versionedRoute, _handler), + HttpVerb.PUT => _endpoints.MapPut(versionedRoute, _handler), + HttpVerb.DELETE => _endpoints.MapDelete(versionedRoute, _handler), + _ => throw new ArgumentOutOfRangeException($"Unconfigured HTTP verb for mapping: {_verb}") + }; + + if (_allowAnonymous) + { + builder.AllowAnonymous(); + } + else + { + if (_authorizationPolicies.Any()) + { + builder.RequireAuthorization(_authorizationPolicies.ToArray()); + } + else if (!string.IsNullOrWhiteSpace(authorizationPolicy)) + { + builder.RequireAuthorization(authorizationPolicy); + } + else + { + builder.RequireAuthorization(); + } + } + + builder.WithGroupName(version.ToString()); + builder.WithResponseCode(401, "Unauthorized. The request requires authentication"); + builder.WithResponseCode(403, "Forbidden. The request is authenticated, but not authorized to access this resource"); + if (display409) + builder.WithResponseCode(409, "Conflict. The request is authenticated, but it has a conflict with an existing element"); + builder.WithResponseCode(500, FeatureCommonConstants.InternalServerErrorResponseDescription); + + if (_route.Contains("id", StringComparison.InvariantCultureIgnoreCase)) + { + builder.WithResponseCode(404, "Not found. A resource with given identifier could not be found."); + } + + if (_verb is HttpVerb.PUT or HttpVerb.POST) + { + builder.WithResponseCode(400, FeatureCommonConstants.BadRequestResponseDescription); + } + + foreach (var action in _routeOptions) + { + action(builder); + } + } + } + + public AdminApiEndpointBuilder WithRouteOptions(Action routeHandlerBuilderAction) + { + _routeOptions.Add(routeHandlerBuilderAction); + return this; + } + + public AdminApiEndpointBuilder WithDefaultSummaryAndDescription() + { + var summary = _verb switch + { + HttpVerb.GET => _route.Contains("id") ? $"Retrieves a specific {_pluralResourceName.ToSingleEntity()} based on the identifier." : $"Retrieves all {_pluralResourceName}.", + HttpVerb.POST => $"Creates {_pluralResourceName.ToSingleEntity()} based on the supplied values.", + HttpVerb.PUT => $"Updates {_pluralResourceName.ToSingleEntity()} based on the resource identifier.", + HttpVerb.DELETE => $"Deletes an existing {_pluralResourceName.ToSingleEntity()} using the resource identifier.", + _ => throw new ArgumentOutOfRangeException($"Unconfigured HTTP verb for default description {_verb}") + }; + + var description = _verb switch + { + HttpVerb.GET => "This GET operation provides access to resources using the \"Get\" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists).", + HttpVerb.POST => "The POST operation can be used to create or update resources. In database terms, this is often referred to as an \"upsert\" operation (insert + update). Clients should NOT include the resource \"id\" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.", + HttpVerb.PUT => "The PUT operation is used to update a resource by identifier. If the resource identifier (\"id\") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.", + HttpVerb.DELETE => "The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found).", + _ => throw new ArgumentOutOfRangeException($"Unconfigured HTTP verb for default description {_verb}") + }; + + return WithSummaryAndDescription(summary, description); + } + + public AdminApiEndpointBuilder WithSummary(string summary) + { + _routeOptions.Add(rhb => rhb.WithMetadata(new SwaggerOperationAttribute(summary))); + return this; + } + + public AdminApiEndpointBuilder WithSummaryAndDescription(string summary, string description) + { + _routeOptions.Add(rhb => rhb.WithMetadata(new SwaggerOperationAttribute(summary, description))); + return this; + } + + public AdminApiEndpointBuilder AllowAnonymous() + { + _allowAnonymous = true; + return this; + } + + private enum HttpVerb { GET, POST, PUT, DELETE } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiVersions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiVersions.cs similarity index 69% rename from Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiVersions.cs rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiVersions.cs index bfdfb8d60..c00e4bf76 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiVersions.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiVersions.cs @@ -6,22 +6,27 @@ using System.Reflection; using Asp.Versioning.Builder; using Asp.Versioning.Conventions; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; -namespace EdFi.Ods.AdminApi.Infrastructure; +namespace EdFi.Ods.AdminApi.Common.Infrastructure; public class AdminApiVersions { private static bool _isInitialized; - public static AdminApiVersion V1 = new(1.1, "v1"); + public static readonly AdminApiVersion V1 = new(1.1, "v1"); + public static readonly AdminApiVersion V2 = new(2.0, "v2"); private static ApiVersionSet? _versionSet; public static void Initialize(WebApplication app) { - if (_isInitialized) throw new InvalidOperationException("Versions are already initialized"); + if (_isInitialized) + throw new InvalidOperationException("Versions are already initialized"); _versionSet = app.NewApiVersionSet() .HasApiVersion(V1.Version) + .HasApiVersion(V2.Version) .Build(); _isInitialized = true; @@ -29,7 +34,7 @@ public static void Initialize(WebApplication app) public static ApiVersionSet VersionSet { - get => _versionSet ?? throw new Exception( + get => _versionSet ?? throw new ArgumentException( "Admin API Versions have not been initialized. Call Initialize() at app startup"); } @@ -50,16 +55,10 @@ public static string[] GetAllVersionStrings() .ToArray(); } - public class AdminApiVersion + public class AdminApiVersion(double version, string displayName) { - public double Version { get; } - public string DisplayName { get; } + public double Version { get; } = version; + public string DisplayName { get; } = displayName; public override string ToString() => DisplayName; - - public AdminApiVersion(double version, string displayName) - { - Version = version; - DisplayName = displayName; - } } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/CloudOdsAdminApp.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/CloudOdsAdminApp.cs similarity index 95% rename from Application/EdFi.Ods.AdminApi/Infrastructure/CloudOdsAdminApp.cs rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/CloudOdsAdminApp.cs index 6e974e19d..ec34dc92b 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/CloudOdsAdminApp.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/CloudOdsAdminApp.cs @@ -3,7 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -namespace EdFi.Ods.AdminApi.Infrastructure; +namespace EdFi.Ods.AdminApi.Common.Infrastructure; public static class Constants { diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/CommonQueryParams.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/CommonQueryParams.cs new file mode 100644 index 000000000..f32e833ed --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/CommonQueryParams.cs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure; + +public struct CommonQueryParams +{ + [FromQuery(Name = "offset")] + public int? Offset { get; set; } + [FromQuery(Name = "limit")] + public int? Limit { get; set; } + [FromQuery(Name = "orderBy")] + public string? OrderBy { get; set; } + [FromQuery(Name = "direction")] + public string? Direction { get; set; } + [BindNever] + public readonly bool IsDescending => SortingDirectionHelper.IsDescendingSorting(Direction); + public CommonQueryParams() { } + public CommonQueryParams(int? offset, int? limit) + { + Offset = offset; + Limit = limit; + } + public CommonQueryParams(int? offset, int? limit, string? orderBy, string? direction) + { + Offset = offset; + Limit = limit; + OrderBy = orderBy; + Direction = SortingDirectionHelper.GetNonEmptyOrDefault(direction); + } + +} + diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Context/ContextProvider.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Context/ContextProvider.cs new file mode 100644 index 000000000..8db7bc20f --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Context/ContextProvider.cs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Context; + +public interface IContextProvider +{ + T? Get(); + void Set(T? context); +} + +public class ContextProvider(IContextStorage contextStorage) : IContextProvider +{ + private static readonly string? _contextKey = typeof(T?).FullName; + + private readonly IContextStorage _contextStorage = contextStorage; + + public T? Get() => _contextStorage.GetValue(_contextKey!); + + public void Set(T? context) => _contextStorage.SetValue(_contextKey!, context!); +} + diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Context/ContextStorage.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Context/ContextStorage.cs new file mode 100644 index 000000000..8cd81f290 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Context/ContextStorage.cs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Context +{ + public interface IContextStorage + { + void SetValue(string key, object value); + + T? GetValue(string key); + } + + public class HashtableContextStorage : IContextStorage + { + + public Hashtable UnderlyingHashtable { get; } = []; + + public void SetValue(string key, object value) + { + UnderlyingHashtable[key] = value; + } + + public T? GetValue(string key) => (T?)(UnderlyingHashtable != null && + UnderlyingHashtable[key] != null ? UnderlyingHashtable[key] : default(T)); + } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/AdminConsolePostgresUsersContext.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/AdminConsolePostgresUsersContext.cs new file mode 100644 index 000000000..3ba048008 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/AdminConsolePostgresUsersContext.cs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Database; diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/AdminConsoleSqlServerUsersContext.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/AdminConsoleSqlServerUsersContext.cs new file mode 100644 index 000000000..6f4712a6c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/AdminConsoleSqlServerUsersContext.cs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Database; + diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs similarity index 87% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs index ed36f6269..f1f9a13d9 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs @@ -3,11 +3,10 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; -namespace EdFi.Ods.AdminApi.Infrastructure.Database; +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Database; public static class EntityFrameworkCoreDatabaseModelBuilderExtensions { @@ -20,7 +19,8 @@ public static void ApplyDatabaseServerSpecificConventions(this ModelBuilder mode foreach (var entity in modelBuilder.Model.GetEntityTypes()) { - if (entity is null) throw new InvalidOperationException("Entity should not be null"); + if (entity is null) + throw new InvalidOperationException("Entity should not be null"); var tableName = entity.GetTableName() ?? throw new InvalidOperationException($"Entity of type {entity.GetType()} has a null table name"); entity.SetTableName(tableName.ToLowerInvariant()); @@ -29,7 +29,7 @@ public static void ApplyDatabaseServerSpecificConventions(this ModelBuilder mode { var tableId = StoreObjectIdentifier.Table(tableName); var columnName = property.GetColumnName(tableId) ?? property.GetDefaultColumnName(tableId); - property.SetColumnName(columnName.ToLowerInvariant()); + property.SetColumnName(columnName?.ToLowerInvariant()); } foreach (var key in entity.GetKeys()) diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/DatabaseEngineEnum.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/DatabaseEngineEnum.cs similarity index 60% rename from Application/EdFi.Ods.AdminApi/Infrastructure/DatabaseEngineEnum.cs rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/DatabaseEngineEnum.cs index fd566682d..bc13a7357 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/DatabaseEngineEnum.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/DatabaseEngineEnum.cs @@ -1,9 +1,15 @@ -namespace EdFi.Ods.AdminApi.Infrastructure; +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Infrastructure; public static class DatabaseEngineEnum { public const string SqlServer = "SqlServer"; public const string PostgreSql = "PostgreSql"; + public const string SqlServerCollation = "Latin1_General_100_BIN2_UTF8"; public static string Parse(string value) { diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/EndpointRouteBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/EndpointRouteBuilderExtensions.cs similarity index 63% rename from Application/EdFi.Ods.AdminApi/Infrastructure/EndpointRouteBuilderExtensions.cs rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/EndpointRouteBuilderExtensions.cs index 0a1879d2f..d39696a4e 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/EndpointRouteBuilderExtensions.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/EndpointRouteBuilderExtensions.cs @@ -1,6 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Swashbuckle.AspNetCore.Annotations; -namespace EdFi.Ods.AdminApi.Infrastructure; +namespace EdFi.Ods.AdminApi.Common.Infrastructure; public static class EndpointRouteBuilderExtensions { diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/AdminAppException.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/AdminApiException.cs similarity index 65% rename from Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/AdminAppException.cs rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/AdminApiException.cs index 059b50e57..22cdec064 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/AdminAppException.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/AdminApiException.cs @@ -1,7 +1,11 @@ -using System; +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + using System.Net; -namespace EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; +namespace EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; public interface IAdminApiException { diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/NotFoundException.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/NotFoundException.cs new file mode 100644 index 000000000..df357a676 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/NotFoundException.cs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +public interface INotFoundException +{ + public string Message { get; } +} + +public class NotFoundException(string resourceName, T id) : Exception($"Not found: {resourceName} with ID {id}. It may have been recently deleted."), INotFoundException +{ + public string ResourceName { get; } = resourceName; + public T Id { get; } = id; +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/OdsApiConnectionException.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/OdsApiConnectionException.cs similarity index 93% rename from Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/OdsApiConnectionException.cs rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/OdsApiConnectionException.cs index 3cdd0f5bd..6d856b06a 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/OdsApiConnectionException.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ErrorHandling/OdsApiConnectionException.cs @@ -3,10 +3,9 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System; using System.Net; -namespace EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; +namespace EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; public class OdsApiConnectionException : Exception, IAdminApiException { diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Extensions/IConfigurationExtensions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Extensions/IConfigurationExtensions.cs new file mode 100644 index 000000000..1ce1515fe --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Extensions/IConfigurationExtensions.cs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.Extensions.Configuration; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; + +public static class IConfigurationExtensions +{ + public static T Get(this IConfiguration configuration, string key, T defaultValue) + { + return configuration.GetValue(key, defaultValue) ?? defaultValue; + } + + public static T Get(this IConfiguration configuration, string key) + { + return configuration.GetValue(key) + ?? throw new AdminApiException($"Unable to load {key} from appSettings"); + } + + public static string GetConnectionStringByName(this IConfiguration configuration, string name) + { + return configuration.GetConnectionString(name) + ?? throw new AdminApiException($"Missing connection string {name}."); + } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Extensions/StringExtensions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Extensions/StringExtensions.cs new file mode 100644 index 000000000..4dde39c06 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Extensions/StringExtensions.cs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; + +public static partial class StringExtensions +{ + public static string ToTitleCase(this string input) + => CultureInfo.CurrentCulture.TextInfo.ToTitleCase(input); + + public static string ToSingleEntity(this string input) + { + return input.Remove(input.Length - 1, 1); + } + + public static string ToDelimiterSeparated(this IEnumerable inputStrings, string separator = ",") + { + var listOfStrings = inputStrings.ToList(); + + return listOfStrings.Count != 0 + ? string.Join(separator, listOfStrings) + : string.Empty; + } + + public static object ToJsonObjectResponseDeleted(this string input) + { + return new { title = $"{input} deleted successfully" }; + } + + public static string ToPascalCase(this string input) + { + var matches = ToPascalCaseRegex().Match(input); + var groupWords = matches.Groups["word"]; + + var thread = Thread.CurrentThread.CurrentCulture.TextInfo; + var stringBuilder = new StringBuilder(); + foreach (var captureWord in groupWords.Captures.Cast()) + stringBuilder.Append(thread.ToTitleCase(captureWord.Value.ToLower())); + return stringBuilder.ToString(); + } + + [GeneratedRegex("^(?^[a-z]+|[A-Z]+|[A-Z][a-z]+)+$")] + private static partial Regex ToPascalCaseRegex(); +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/ConnectionStringHelper.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/ConnectionStringHelper.cs new file mode 100644 index 000000000..b9ba02d5c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/ConnectionStringHelper.cs @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using log4net; +using Microsoft.Data.SqlClient; +using Npgsql; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; + +public static class ConnectionStringHelper +{ + private static readonly ILog _log = LogManager.GetLogger(typeof(ConnectionStringHelper)); + public static bool ValidateConnectionString(string databaseEngine, string? connectionString) + { + bool result = true; + if (databaseEngine.Equals(DatabaseEngineEnum.SqlServer, StringComparison.InvariantCultureIgnoreCase)) + { + try + { + _ = new SqlConnectionStringBuilder(connectionString); + } + catch (Exception ex) + { + if (ex is ArgumentException || + ex is FormatException || + ex is KeyNotFoundException) + { + result = false; + _log.Error(ex); + } + } + } + else if (databaseEngine.Equals(DatabaseEngineEnum.PostgreSql, StringComparison.InvariantCultureIgnoreCase)) + { + try + { + _ = new NpgsqlConnectionStringBuilder(connectionString); + } + catch (ArgumentException ex) + { + result = false; + _log.Error(ex); + } + } + return result; + } + + public static (string? Host, string? Database) GetHostAndDatabase(string databaseEngine, string? connectionString) + { + if (databaseEngine.Equals(DatabaseEngineEnum.SqlServer, StringComparison.InvariantCultureIgnoreCase)) + { + try + { + var builder = new SqlConnectionStringBuilder(connectionString); + return (builder.DataSource, builder.InitialCatalog); + } + catch (Exception ex) when ( + ex is ArgumentException or + FormatException or + KeyNotFoundException) + { + _log.Error(ex); + return (null, null); + } + } + else if (databaseEngine.Equals(DatabaseEngineEnum.PostgreSql, StringComparison.InvariantCultureIgnoreCase)) + { + try + { + var builder = new NpgsqlConnectionStringBuilder(connectionString); + return (builder.Host, builder.Database); + } + catch (ArgumentException ex) + { + _log.Error(ex); + return (null, null); + } + } + + return (null, null); + } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/FeaturesHelper.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/FeaturesHelper.cs new file mode 100644 index 000000000..243a91bfb --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/FeaturesHelper.cs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using System.Reflection; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; + +public static class FeatureHelper +{ + public static List GetFeatures(Assembly executingAssembly) + { + var featureInterface = typeof(IFeature); + var featureImpls = executingAssembly.GetTypes() + .Where(p => featureInterface.IsAssignableFrom(p) && p.IsClass); + + var features = new List(); + + foreach (var featureImpl in featureImpls) + { + if (Activator.CreateInstance(featureImpl) is IFeature feature) + features.Add(feature); + } + return features; + } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/QueryStringMappingHelper.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/QueryStringMappingHelper.cs new file mode 100644 index 000000000..193b5221a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/QueryStringMappingHelper.cs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq.Expressions; +using FluentValidation; +using FluentValidation.Results; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; + +public static class QueryStringMappingHelper +{ + public static Expression> GetColumnToOrderBy(this Dictionary>> mapping, string? orderBy) + { + orderBy ??= string.Empty; + + if (mapping != null) + { + if (string.IsNullOrEmpty(orderBy)) + return mapping.First().Value; + + if (!mapping.TryGetValue(orderBy.ToLowerInvariant(), out Expression>? result)) + { + throw new ValidationException([new ValidationFailure(nameof(orderBy), $"The orderBy value {orderBy} is not allowed. The allowed values are {string.Join(",", mapping.Keys)}")]); + } + + return result; + } + + throw new ArgumentNullException(nameof(mapping)); + } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/SortingDirectionHelper.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/SortingDirectionHelper.cs new file mode 100644 index 000000000..fee8d061b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/SortingDirectionHelper.cs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel; +using System.Runtime.Serialization; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; + +public static class SortingDirectionHelper +{ + public enum Direction + { + [Description("Ascending")] + [EnumMember(Value = "Asc")] + Ascending, + + [Description("Descending")] + [EnumMember(Value = "Desc")] + Descending + } + + public static bool IsDescendingSorting(string? direction) + { + direction = GetNonEmptyOrDefault(direction); + + bool result = direction.ToLowerInvariant() switch + { + "asc" or "ascending" => false, + "desc" or "descending" => true, + _ => false, + }; + + return result; + } + + public static string GetNonEmptyOrDefault(string? direction) + { + if (!string.IsNullOrEmpty(direction)) + return direction; + return Direction.Ascending.ToString(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/StringToJsonDocumentConverter.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/StringToJsonDocumentConverter.cs new file mode 100644 index 000000000..a110192ac --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/StringToJsonDocumentConverter.cs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Helpers +{ + public class StringToJsonDocumentConverter : JsonConverter + { + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetString(); + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + JsonDocument jsonDocument = JsonDocument.Parse(value); + jsonDocument.WriteTo(writer); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/InstanceContext.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/InstanceContext.cs similarity index 89% rename from Application/EdFi.Ods.AdminApi/Infrastructure/InstanceContext.cs rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/InstanceContext.cs index 4e145cfc5..958489ae1 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/InstanceContext.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/InstanceContext.cs @@ -3,7 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -namespace EdFi.Ods.AdminApi.Infrastructure; +namespace EdFi.Ods.AdminApi.Common.Infrastructure; public class InstanceContext { diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantConfiguration.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantConfiguration.cs new file mode 100644 index 000000000..8c1e6e616 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantConfiguration.cs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; + +public class TenantConfiguration +{ + public string? TenantIdentifier { get; set; } + + public string? AdminConnectionString { get; set; } + + public string? SecurityConnectionString { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantConfigurationProvider.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantConfigurationProvider.cs new file mode 100644 index 000000000..54f62d369 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantConfigurationProvider.cs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Settings; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; + +public interface ITenantConfigurationProvider +{ + IDictionary Get(); +} + +public class TenantConfigurationProvider : ITenantConfigurationProvider +{ + private IDictionary _tenantConfigurationByIdentifier; + + public TenantConfigurationProvider(IOptionsMonitor tenantsConfigurationOptions) + { + _tenantConfigurationByIdentifier = InitializeTenantsConfiguration(tenantsConfigurationOptions.CurrentValue); + + tenantsConfigurationOptions.OnChange(config => + { + var newMap = InitializeTenantsConfiguration(config); + Interlocked.Exchange(ref _tenantConfigurationByIdentifier, newMap); + }); + } + + private static Dictionary InitializeTenantsConfiguration(TenantsSection config) + { + return config.Tenants.ToDictionary( + t => t.Key, + t => new TenantConfiguration + { + TenantIdentifier = t.Key, + AdminConnectionString = t.Value.ConnectionStrings.GetValueOrDefault("EdFi_Admin"), + SecurityConnectionString = t.Value.ConnectionStrings.GetValueOrDefault("EdFi_Security"), + }, + StringComparer.OrdinalIgnoreCase); + } + + public IDictionary Get() => _tenantConfigurationByIdentifier; +} + diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantIdentificationMiddleware.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantIdentificationMiddleware.cs new file mode 100644 index 000000000..83ddfd1f3 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantIdentificationMiddleware.cs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Text.RegularExpressions; +using EdFi.Ods.AdminApi.Common.Infrastructure.Context; +using EdFi.Ods.AdminApi.Common.Settings; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; + +public partial class TenantResolverMiddleware( + ITenantConfigurationProvider tenantConfigurationProvider, + IContextProvider tenantConfigurationContextProvider, + IOptions options, + IOptions swaggerOptions) : IMiddleware +{ + private readonly ITenantConfigurationProvider _tenantConfigurationProvider = tenantConfigurationProvider; + private readonly IContextProvider _tenantConfigurationContextProvider = tenantConfigurationContextProvider; + private readonly IOptions _options = options; + private readonly IOptions _swaggerOptions = swaggerOptions; + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var apiMode = _options.Value.AdminApiMode?.ToLower() ?? "v2"; + var multiTenancyEnabled = _options.Value.MultiTenancy; + var validationErrorMessage = "Please provide valid tenant id. Tenant id can only contain alphanumeric and -"; + + // Check if this is a V1 endpoint + if (IsV1Mode(apiMode)) + { + // For V1 endpoints, skip multi-tenancy validation entirely + await next.Invoke(context); + return; + } + if (multiTenancyEnabled) + { + if (context.Request.Headers.TryGetValue("tenant", out var tenantIdentifier) && + !string.IsNullOrEmpty(tenantIdentifier)) + { + if (IsValidTenantId(tenantIdentifier!)) + { + if (_tenantConfigurationProvider.Get().TryGetValue(tenantIdentifier!, out var tenantConfiguration)) + { + _tenantConfigurationContextProvider.Set(tenantConfiguration); + } + else + { + ThrowTenantValidationError($"Tenant not found with provided tenant id: {tenantIdentifier}"); + } + } + else + { + ThrowTenantValidationError(validationErrorMessage); + } + } + else if (_swaggerOptions.Value.EnableSwagger && RequestFromSwagger()) + { + var defaultTenant = _swaggerOptions.Value.DefaultTenant; + if (!string.IsNullOrEmpty(defaultTenant) && IsValidTenantId(defaultTenant)) + { + if (!string.IsNullOrEmpty(defaultTenant) && + _tenantConfigurationProvider.Get().TryGetValue(defaultTenant, out var tenantConfiguration)) + { + _tenantConfigurationContextProvider.Set(tenantConfiguration); + } + else + { + ThrowTenantValidationError("Please configure valid default tenant id"); + } + } + else + { + ThrowTenantValidationError(validationErrorMessage); + } + } + else + { + if (!context.Request.Path.Value!.Contains("adminconsole/tenants") && + context.Request.Method != "GET" && + !context.Request.Path.Value.Contains("health", StringComparison.InvariantCultureIgnoreCase)) + { + ThrowTenantValidationError("Tenant header is missing (adminconsole)"); + } + } + } + await next.Invoke(context); + + bool RequestFromSwagger() => (context.Request.Path.Value != null && + context.Request.Path.Value.Contains("swagger", StringComparison.InvariantCultureIgnoreCase)) || + context.Request.Headers.Referer.FirstOrDefault(x => x != null && x.ToLower().Contains("swagger", StringComparison.InvariantCultureIgnoreCase)) != null; + + void ThrowTenantValidationError(string errorMessage) + { + throw new ValidationException([new ValidationFailure("Tenant", errorMessage)]); + } + } + + private static bool IsV1Mode(string _adminApiMode) + { + return string.Equals(_adminApiMode, "v1", StringComparison.InvariantCultureIgnoreCase); + } + + private static bool IsValidTenantId(string tenantId) + { + const int MaxLength = 50; + var regex = IsValidTenantIdRegex(); + + if (string.IsNullOrEmpty(tenantId) || tenantId.Length > MaxLength || + !regex.IsMatch(tenantId)) + { + return false; + } + return true; + } + + [GeneratedRegex("^[A-Za-z0-9-]+$")] + private static partial Regex IsValidTenantIdRegex(); +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/OperationalContext.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/OperationalContext.cs similarity index 79% rename from Application/EdFi.Ods.AdminApi/Infrastructure/OperationalContext.cs rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/OperationalContext.cs index de8412f09..881468569 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/OperationalContext.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/OperationalContext.cs @@ -3,9 +3,9 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -namespace EdFi.Ods.AdminApi.Infrastructure; +namespace EdFi.Ods.AdminApi.Common.Infrastructure; -public class OperationalContext +public static class OperationalContext { public const string DefaultOperationalContextUri = "uri://ed-fi-api-host.org"; } diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Providers/Aes256SymmetricStringEncryptionProvider.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Providers/Aes256SymmetricStringEncryptionProvider.cs new file mode 100644 index 000000000..07f84607d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Providers/Aes256SymmetricStringEncryptionProvider.cs @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Security.Cryptography; +using EdFi.Ods.AdminApi.Common.Infrastructure.Providers.Interfaces; +using log4net; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Providers; + +/// +/// Implements AES 256 bit symmetric key encryption of string values. +/// +/// +/// This class is used to facilitate the encryption of string values using the AES 256 bit +/// encryption algorithm. HMAC signing of the encrypted value is used to mitigate potential +/// timing-based padding oracle attacks on AES in CBC mode (as discussed here +/// https://learn.microsoft.com/en-us/dotnet/standard/security/vulnerabilities-cbc-mode) +/// by providing a means of verifying the authenticity of the encrypted content before decryption is attempted. +public class Aes256SymmetricStringEncryptionProvider : ISymmetricStringEncryptionProvider +{ + + private readonly ILog _logger; + + public Aes256SymmetricStringEncryptionProvider() + { + _logger = LogManager.GetLogger(typeof(Aes256SymmetricStringEncryptionProvider)); + } + + /// + /// Encrypt the specified string value using the specified key. + /// + /// The string value to be encrypted. + /// The 256 bit private key to be used for encryption. + /// A string representing the input in encrypted form along with the other information + /// necessary to decrypt it (aside from the private key). The output is three strings concatenated in + /// the format "IV|EncryptedMessage|HMACSignature" + public string Encrypt(string? value, byte[]? key) + { + // Incorporates code from https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.aes?view=net-6.0 + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Input value cannot be null or whitespace.", nameof(value)); + } + + if (key == null || key.Length != 32) + { + throw new ArgumentException("Key must be 256 bits (32 bytes) in length.", nameof(key)); + } + + using Aes aesInstance = Aes.Create(); + + aesInstance.Key = key; + aesInstance.GenerateIV(); + byte[] encryptedBytes; + + ICryptoTransform encryptor = aesInstance.CreateEncryptor(aesInstance.Key, aesInstance.IV); + + using (MemoryStream msEncrypt = new()) + { + using CryptoStream csEncrypt = new(msEncrypt, encryptor, CryptoStreamMode.Write); + using (StreamWriter swEncrypt = new(csEncrypt)) + { + swEncrypt.Write(value); + } + + encryptedBytes = msEncrypt.ToArray(); + } + + byte[] hmacSignatureValue = GenerateHmacSignature(); + + return + $"{Convert.ToBase64String(aesInstance.IV)}|{Convert.ToBase64String(encryptedBytes)}|{Convert.ToBase64String(hmacSignatureValue)}"; + + byte[] GenerateHmacSignature() + { + byte[] hmacSignature; + + using (HMACSHA256 hmac = new(key)) + { + hmacSignature = hmac.ComputeHash(encryptedBytes); + } + + return hmacSignature; + } + } + + /// + /// Decrypt a string value using the specified key. + /// + /// The data to be decrypted and related information as three base64 + /// encoded segments concatenated in the format "IV|EncryptedMessage|HMACSignature" + /// The 256 bit private key to be used for decryption. + /// If decryption is successful, then the plaintext output, otherwise null + /// A boolean value indicating if decryption was successful. + public bool TryDecrypt(string value, byte[] key, out string? output) + { + // Includes code from https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.aes?view=net-6.0 + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Input value cannot be null or whitespace.", nameof(value)); + } + + if (key == null || key.Length != 32) + { + throw new ArgumentException("Key must be 256 bits (32 bytes) in length.", nameof(key)); + } + + string[] inputParts = value.Split('|'); + + if (inputParts.Length != 3) + { + output = null; + return false; + } + + string base64Iv = inputParts[0]; + string base64EncryptedBytes = inputParts[1]; + string base64HashValue = inputParts[2]; + byte[] iv; + byte[] encryptedBytes; + byte[] hashValue; + + try + { + iv = Convert.FromBase64String(base64Iv); + encryptedBytes = Convert.FromBase64String(base64EncryptedBytes); + hashValue = Convert.FromBase64String(base64HashValue); + } + catch (Exception e) + { + _logger.Error("Unable to convert encryption key from Base 64 String.", e); + throw new ArgumentException("Unable to convert key from Base 64 String.", nameof(key)); + } + + bool signatureIsValid = IsHmacSignatureValid(); + + if (!signatureIsValid) + { + output = null; + return false; + } + + using Aes aesInstance = Aes.Create(); + + aesInstance.Key = key; + aesInstance.IV = iv; + + ICryptoTransform decryptor = aesInstance.CreateDecryptor(aesInstance.Key, aesInstance.IV); + + using MemoryStream msDecrypt = new(encryptedBytes); + using CryptoStream csDecrypt = new(msDecrypt, decryptor, CryptoStreamMode.Read); + using StreamReader srDecrypt = new(csDecrypt); + output = srDecrypt.ReadToEnd(); + + return true; + + bool IsHmacSignatureValid() + { + bool signatureCheckResult; + + using (HMACSHA256 hmac = new(key)) + { + byte[] computedHashValue = hmac.ComputeHash(encryptedBytes); + signatureCheckResult = hashValue.SequenceEqual(computedHashValue); + } + + return signatureCheckResult; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Providers/Interfaces/ISymmetricStringEncryptionProvider.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Providers/Interfaces/ISymmetricStringEncryptionProvider.cs new file mode 100644 index 000000000..ff9f3e326 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Providers/Interfaces/ISymmetricStringEncryptionProvider.cs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Providers.Interfaces; + +/// +/// Defines an interface for providing symmetric encryption of string values. +/// +/// +/// This interface and its implementations are used to facilitate the encryption and decryption +/// of string values using a symmetric encryption algorithm. +public interface ISymmetricStringEncryptionProvider +{ + /// + /// Encrypts the specified string value using the specified key. + /// + /// The string value to be encrypted. + /// The private key to be used for encryption. + /// A string representing the input in encrypted form along with any + /// other information necessary to decrypt it (aside from the private key) + public string Encrypt(string? value, byte[]? key); + + /// + /// Decrypts the specified string value using the specified key. + /// + /// The string value to be decrypted. + /// The private key to be used for decryption. + /// TThe decrypted string value + /// Indicates if the decryption operation was successful + public bool TryDecrypt(string value, byte[] key, out string? output); +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Security/AuthorizationPolicies.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Security/AuthorizationPolicies.cs new file mode 100644 index 000000000..0723f015d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Security/AuthorizationPolicies.cs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Data; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Security +{ + public static class AuthorizationPolicies + { + // Create policies by scope + public static readonly PolicyDefinition AdminApiFullAccessScopePolicy = new PolicyDefinition("AdminApiFullAccessScopePolicy", SecurityConstants.Scopes.AdminApiFullAccess.Scope); + public static readonly PolicyDefinition DefaultScopePolicy = AdminApiFullAccessScopePolicy; + public static readonly IEnumerable ScopePolicies = new List + { + AdminApiFullAccessScopePolicy, + }; + } + + public class PolicyDefinition + { + public string PolicyName { get; } + public string Scope { get; } + + public PolicyDefinition(string policyName, string scope) + { + PolicyName = policyName; + Scope = scope; + } + public override string ToString() + { + return this.PolicyName; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Security/SecurityConstants.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Security/SecurityConstants.cs new file mode 100644 index 000000000..e06c50870 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Security/SecurityConstants.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Security; + +public static class SecurityConstants +{ + public const string ConnectRoute = "connect"; + public const string TokenActionName = "token"; + public const string TokenEndpoint = $"{ConnectRoute}/{TokenActionName}"; + public const string RegisterActionName = "register"; + public const string DefaultRoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"; + + public static class Scopes + { + public static readonly ScopeDefinition AdminApiFullAccess = new("edfi_admin_api/full_access", "Full access to the Admin API"); + + public static IEnumerable AllScopes = + [ + AdminApiFullAccess + ]; + } + + public class ScopeDefinition(string scope, string scopeDescription) + { + public string Scope { get; } = scope; + public string ScopeDescription { get; } = scopeDescription; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/OdsApiValidator.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Services/OdsApiValidator.cs similarity index 97% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/OdsApiValidator.cs rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/Services/OdsApiValidator.cs index e74f567a8..cdaa99ad7 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/OdsApiValidator.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Services/OdsApiValidator.cs @@ -4,13 +4,12 @@ // See the LICENSE and NOTICES files in the project root for more information. using System.Net; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using EdFi.Ods.AdminApi.Infrastructure.Services; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NJsonSchema; -namespace EdFi.Ods.AdminApi.Infrastructure.Api; +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Services; public interface IOdsApiValidator { diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Services/SimpleGetRequest.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Services/SimpleGetRequest.cs new file mode 100644 index 000000000..511483955 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Services/SimpleGetRequest.cs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Services; + +public interface ISimpleGetRequest +{ + public Task DownloadString(string address); +} + +public class SimpleGetRequest : ISimpleGetRequest +{ + private readonly IHttpClientFactory _clientFactory; + + public SimpleGetRequest(IHttpClientFactory clientFactory) + { + _clientFactory = clientFactory; + } + + public async Task DownloadString(string address) + { + var httpClient = _clientFactory.CreateClient(); + + using (var response = await httpClient.GetAsync(address)) + { + await CheckResponseStatusCode(address, response); + + using (var content = response.Content) + { + return await content.ReadAsStringAsync(); + } + } + + static async Task CheckResponseStatusCode(string requestUrl, HttpResponseMessage response) + { + switch (response.StatusCode) + { + case HttpStatusCode.OK: + break; + case 0: + throw new HttpRequestException($"No response from {requestUrl}."); + case HttpStatusCode.NotFound: + throw new HttpRequestException($"{requestUrl} not found.", null, HttpStatusCode.NotFound); + case HttpStatusCode.ServiceUnavailable: + throw new HttpRequestException( + $"{requestUrl} is unavailable", null, HttpStatusCode.ServiceUnavailable); + case HttpStatusCode.BadGateway: + throw new HttpRequestException( + $"{requestUrl} was acting as a gateway or proxy and received an invalid response from the upstream server.", + null, HttpStatusCode.BadGateway); + case (HttpStatusCode)495: + throw new HttpRequestException( + $"Invalid SSL client certificate for {requestUrl}.", null, (HttpStatusCode)495); + case (HttpStatusCode)496: + throw new HttpRequestException( + $"Missing SSL client certificate for {requestUrl}.", null, (HttpStatusCode)496); + case HttpStatusCode.BadRequest: + throw new InvalidOperationException($"Malformed request for {requestUrl}."); + default: + var message = $"Unexpected response from {requestUrl}"; + + using (var content = response.Content) + { + var details = await content.ReadAsStringAsync(); + + if (!string.IsNullOrEmpty(details)) + message += $": {details}"; + } + + throw new HttpRequestException(message, null, HttpStatusCode.ServiceUnavailable); + } + } + } +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/ValidatorExtensions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ValidatorExtensions.cs similarity index 92% rename from Application/EdFi.Ods.AdminApi/Infrastructure/ValidatorExtensions.cs rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/ValidatorExtensions.cs index f2600ee2d..265bfa9c8 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/ValidatorExtensions.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ValidatorExtensions.cs @@ -5,6 +5,8 @@ using FluentValidation; +namespace EdFi.Ods.AdminApi.Common.Infrastructure; + public static class ValidatorExtensions { public static async Task GuardAsync(this IValidator validator, TRequest request) diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs new file mode 100644 index 000000000..635140d3c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Settings; + +public class AppSettingsFile +{ + public required AppSettings AppSettings { get; set; } + public required SwaggerSettings SwaggerSettings { get; set; } + public Dictionary ConnectionStrings { get; set; } = []; + public Dictionary Tenants { get; set; } = new(StringComparer.OrdinalIgnoreCase); + public required TestingSettings Testing { get; set; } +} + +public class AppSettings +{ + public int DefaultPageSizeOffset { get; set; } + public int DefaultPageSizeLimit { get; set; } + public string? DatabaseEngine { get; set; } + public string? EncryptionKey { get; set; } + public bool MultiTenancy { get; set; } + public bool PreventDuplicateApplications { get; set; } + public bool EnableApplicationResetEndpoint { get; set; } + public string? AdminApiMode { get; set; } +} + +public class SwaggerSettings +{ + public bool EnableSwagger { get; set; } + public string? DefaultTenant { get; set; } +} + + +public class IpRateLimitingOptions +{ + public bool EnableEndpointRateLimiting { get; set; } + public bool StackBlockedRequests { get; set; } + public string RealIpHeader { get; set; } = string.Empty; + public string ClientIdHeader { get; set; } = string.Empty; + public int HttpStatusCode { get; set; } + public List? IpWhitelist { get; set; } + public List? EndpointWhitelist { get; set; } + public List? GeneralRules { get; set; } + + public class GeneralRule + { + public string Endpoint { get; set; } = string.Empty; + public string Period { get; set; } = string.Empty; + public int Limit { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/CorsSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/CorsSettings.cs new file mode 100644 index 000000000..c3c247e70 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Settings/CorsSettings.cs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Settings; + +public class CorsSettings +{ + public bool EnableCors { get; set; } + public string[] AllowedOrigins { get; set; } = []; +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/FileHelpers.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/FileHelpers.cs new file mode 100644 index 000000000..484b70da6 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Settings/FileHelpers.cs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Settings; + +public static class FileHelpers +{ + public static byte[] ComputeHash(string filePath) + { + var runCount = 1; + + while (runCount < 4) + { + try + { + if (File.Exists(filePath)) + { + using var fs = File.OpenRead(filePath); + return System.Security.Cryptography.SHA1 + .Create().ComputeHash(fs); + } + else + { + throw new FileNotFoundException(); + } + } + catch (IOException) + { + if (runCount == 3) + { + throw; + } + + Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount))); + runCount++; + } + } + + return new byte[20]; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Helpers/AppSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/IEncryptionKeySettings.cs similarity index 62% rename from Application/EdFi.Ods.AdminApi/Helpers/AppSettings.cs rename to Application/EdFi.Ods.AdminApi.Common/Settings/IEncryptionKeySettings.cs index 58b10f1b3..3c111419b 100644 --- a/Application/EdFi.Ods.AdminApi/Helpers/AppSettings.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Settings/IEncryptionKeySettings.cs @@ -3,9 +3,9 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -namespace EdFi.Ods.AdminApi.Helpers; -public class AppSettings +namespace EdFi.Ods.AdminApi.Common.Settings; + +public interface IEncryptionKeySettings { - public int DefaultPageSizeOffset { get; set; } - public int DefaultPageSizeLimit { get; set; } + public string EncryptionKey { get; set; } } diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/TenantSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/TenantSettings.cs new file mode 100644 index 000000000..284a4758a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Settings/TenantSettings.cs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Common.Settings; + +public class TenantsSection +{ + public Dictionary Tenants { get; set; } = new(StringComparer.OrdinalIgnoreCase); +} + +public class TenantSettings +{ + public Dictionary ConnectionStrings { get; set; } = new(StringComparer.OrdinalIgnoreCase); +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/TestingSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/TestingSettings.cs new file mode 100644 index 000000000..90b239a4d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common/Settings/TestingSettings.cs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Common.Settings; + +public class TestingSettings +{ + public bool InjectException { get; set; } + + public void CheckIfHasToThrowException() + { + if (InjectException) + throw new AdminApiException("Exception to test"); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandV6ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandTests.cs similarity index 74% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandV6ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandTests.cs index e7438f219..a94ee4d77 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandV6ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandTests.cs @@ -3,35 +3,27 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System; -using System.Linq; -using NUnit.Framework; using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using NUnit.Framework; using Shouldly; -using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; -using Application = EdFi.Security.DataAccess.Models.Application; +using System.Linq; +using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; [TestFixture] -public class AddClaimSetCommandV6ServiceTests : SecurityDataTestBase +public class AddClaimSetCommandTests : SecurityDataTestBase { [Test] public void ShouldAddClaimSet() { - var testApplication = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(testApplication); - var newClaimSet = new AddClaimSetModel { ClaimSetName = "TestClaimSet" }; var addedClaimSetId = 0; ClaimSet addedClaimSet = null; using (var securityContext = TestContext) { - var command = new AddClaimSetCommandV6Service(securityContext); + var command = new AddClaimSetCommand(securityContext); addedClaimSetId = command.Execute(newClaimSet); addedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == addedClaimSetId); } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/CopyClaimSetCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/CopyClaimSetCommandTests.cs new file mode 100644 index 000000000..5f3b66d80 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/CopyClaimSetCommandTests.cs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Ods.AdminApp.Management.ClaimSetEditor; +using Moq; +using NUnit.Framework; +using Shouldly; +using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; + +namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; + +[TestFixture] +public class CopyClaimSetCommandTests : SecurityDataTestBase +{ + [Test] + public void ShouldCopyClaimSet() + { + var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" }; + Save(testClaimSet); + + var testResourceClaims = SetupClaimSetResourceClaimActions(testClaimSet, + UniqueNameList("ParentRc", 3), UniqueNameList("ChildRc", 1)); + + var newClaimSet = new Mock(); + newClaimSet.Setup(x => x.Name).Returns("TestClaimSet_Copy"); + newClaimSet.Setup(x => x.OriginalId).Returns(testClaimSet.ClaimSetId); + + var copyClaimSetId = 0; + ClaimSet copiedClaimSet = null; + using var securityContext = TestContext; + var command = new CopyClaimSetCommand(securityContext); + copyClaimSetId = command.Execute(newClaimSet.Object); + copiedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == copyClaimSetId); + + copiedClaimSet.ClaimSetName.ShouldBe(newClaimSet.Object.Name); + copiedClaimSet.ForApplicationUseOnly.ShouldBe(false); + copiedClaimSet.IsEdfiPreset.ShouldBe(false); + + var results = ResourceClaimsForClaimSet(copiedClaimSet.ClaimSetId).ToList(); + + var testParentResourceClaimsForId = + testResourceClaims.Where(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId && x.ResourceClaim.ParentResourceClaim == null).Select(x => x.ResourceClaim).ToArray(); + + results.Count.ShouldBe(testParentResourceClaimsForId.Length); + results.Select(x => x.Name).ShouldBe(testParentResourceClaimsForId.Select(x => x.ResourceName), true); + results.Select(x => x.Id).ShouldBe(testParentResourceClaimsForId.Select(x => x.ResourceClaimId), true); + results.All(x => x.Actions.All(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true); + + foreach (var testParentResourceClaim in testParentResourceClaimsForId) + { + var testChildren = securityContext.ResourceClaims.Where(x => + x.ParentResourceClaimId == testParentResourceClaim.ResourceClaimId).ToList(); + var parentResult = results.First(x => x.Id == testParentResourceClaim.ResourceClaimId); + parentResult.Children.Select(x => x.Name).ShouldBe(testChildren.Select(x => x.ResourceName), true); + parentResult.Children.Select(x => x.Id).ShouldBe(testChildren.Select(x => x.ResourceClaimId), true); + parentResult.Children.All(x => x.Actions.All(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandTests.cs similarity index 69% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandV53ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandTests.cs index b418a9bd6..fda9c96a7 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandV53ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandTests.cs @@ -2,54 +2,45 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; -using System; -using System.Linq; -using NUnit.Framework; using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; using Moq; +using NUnit.Framework; using Shouldly; -using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet; -using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application; +using System.Linq; +using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; [TestFixture] -public class DeleteClaimSetCommandV53ServiceTests : SecurityData53TestBase +public class DeleteClaimSetCommandTests : SecurityDataTestBase { [Test] public void ShouldDeleteClaimSet() { - var testApplication = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(testApplication); - var testClaimSetToDelete = new ClaimSet - { ClaimSetName = "TestClaimSet_Delete", Application = testApplication }; + { ClaimSetName = "TestClaimSet_Delete" }; Save(testClaimSetToDelete); - SetupParentResourceClaimsWithChildren(testClaimSetToDelete, testApplication); + SetupClaimSetResourceClaimActions(testClaimSetToDelete, UniqueNameList("ParentRc", 3), UniqueNameList("ChildRc", 1)); var testClaimSetToPreserve = new ClaimSet - { ClaimSetName = "TestClaimSet_Preserve", Application = testApplication }; + { ClaimSetName = "TestClaimSet_Preserve" }; Save(testClaimSetToPreserve); - var resourceClaimsForPreservedClaimSet = SetupParentResourceClaimsWithChildren(testClaimSetToPreserve, testApplication); + var resourceClaimsForPreservedClaimSet = SetupClaimSetResourceClaimActions(testClaimSetToPreserve, UniqueNameList("ParentRc", 3), + UniqueNameList("ChildRc", 1)); var deleteModel = new Mock(); deleteModel.Setup(x => x.Name).Returns(testClaimSetToDelete.ClaimSetName); deleteModel.Setup(x => x.Id).Returns(testClaimSetToDelete.ClaimSetId); using var securityContext = TestContext; - var command = new DeleteClaimSetCommandV53Service(securityContext); + var command = new DeleteClaimSetCommand(securityContext); command.Execute(deleteModel.Object); - var deletedClaimSet = securityContext.ClaimSets.SingleOrDefault(x => x.ClaimSetId == testClaimSetToDelete.ClaimSetId); deletedClaimSet.ShouldBeNull(); - var deletedClaimSetResourceActions = securityContext.ClaimSetResourceClaims.Count(x => x.ClaimSet.ClaimSetId == testClaimSetToDelete.ClaimSetId); + var deletedClaimSetResourceActions = securityContext.ClaimSetResourceClaimActions.Count(x => x.ClaimSet.ClaimSetId == testClaimSetToDelete.ClaimSetId); deletedClaimSetResourceActions.ShouldBe(0); var preservedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == testClaimSetToPreserve.ClaimSetId); @@ -63,7 +54,7 @@ public void ShouldDeleteClaimSet() results.Count.ShouldBe(testParentResourceClaimsForId.Length); results.Select(x => x.Name).ShouldBe(testParentResourceClaimsForId.Select(x => x.ResourceName), true); results.Select(x => x.Id).ShouldBe(testParentResourceClaimsForId.Select(x => x.ResourceClaimId), true); - results.All(x => x.Create).ShouldBe(true); + results.All(x => x.Actions != null && x.Actions.Any(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true); foreach (var testParentResourceClaim in testParentResourceClaimsForId) { @@ -72,20 +63,14 @@ public void ShouldDeleteClaimSet() var parentResult = results.First(x => x.Id == testParentResourceClaim.ResourceClaimId); parentResult.Children.Select(x => x.Name).ShouldBe(testChildren.Select(x => x.ResourceName), true); parentResult.Children.Select(x => x.Id).ShouldBe(testChildren.Select(x => x.ResourceClaimId), true); - parentResult.Children.All(x => x.Create).ShouldBe(true); + parentResult.Children.All(x => x.Actions != null && x.Actions.Any(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true); } } [Test] public void ShouldThrowExceptionOnEditSystemReservedClaimSet() { - var testApplication = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(testApplication); - - var systemReservedClaimSet = new ClaimSet { ClaimSetName = "SIS Vendor", Application = testApplication }; + var systemReservedClaimSet = new ClaimSet { ClaimSetName = "SIS Vendor", IsEdfiPreset = true }; Save(systemReservedClaimSet); var deleteModel = new Mock(); @@ -94,10 +79,10 @@ public void ShouldThrowExceptionOnEditSystemReservedClaimSet() using var securityContext = TestContext; var exception = Assert.Throws(() => { - var command = new DeleteClaimSetCommandV53Service(securityContext); + var command = new DeleteClaimSetCommand(securityContext); command.Execute(deleteModel.Object); }); exception.ShouldNotBeNull(); - exception.Message.ShouldBe($"Claim set({systemReservedClaimSet.ClaimSetName}) is system reserved.Can not be deleted."); + exception.Message.ShouldBe($"Claim set({systemReservedClaimSet.ClaimSetName}) is system reserved. Can not be deleted."); } } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteResourceClaimOnClaimSetCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteResourceClaimOnClaimSetCommandTests.cs new file mode 100644 index 000000000..5185ded30 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteResourceClaimOnClaimSetCommandTests.cs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using NUnit.Framework; +using Shouldly; +using System.Linq; +using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; + +namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; + +[TestFixture] +public class DeleteResourceClaimOnClaimSetCommandTests : SecurityDataTestBase +{ + [Test] + public void ShouldDeleteResourceClaimOnClaimSet() + { + var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" }; + Save(testClaimSet); + + var parentRcNames = UniqueNameList("ParentRc", 2); + var testResources = SetupClaimSetResourceClaimActions(testClaimSet, parentRcNames, UniqueNameList("ChildRc", 1)); + + using var securityContext = TestContext; + var command = new DeleteResouceClaimOnClaimSetCommand(securityContext); + command.Execute(testClaimSet.ClaimSetId, testResources.First().ResourceClaimId); + + var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); + + resourceClaimsForClaimSet.Count.ShouldBeLessThan(testResources.Count); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandTests.cs new file mode 100644 index 000000000..86f6a3bf4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandTests.cs @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Features.ClaimSets; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Security.DataAccess.Contexts; +using NUnit.Framework; +using Shouldly; +using System; +using System.Linq; +using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; +using VendorApplication = EdFi.Admin.DataAccess.Models.Application; +using EdFi.Ods.AdminApi.Common.Infrastructure; + +namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; + +[TestFixture] +public class EditClaimSetCommandTests : SecurityDataTestBase +{ + [Test] + public void ShouldEditClaimSet() + { + var alreadyExistingClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" }; + Save(alreadyExistingClaimSet); + + var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSetEdited", ClaimSetId = alreadyExistingClaimSet.ClaimSetId }; + + using var securityContext = TestContext; + UsersTransaction((usersContext) => + { + var command = new EditClaimSetCommand(securityContext, usersContext); + command.Execute(editModel); + }); + + var editedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == alreadyExistingClaimSet.ClaimSetId); + editedClaimSet.ClaimSetName.ShouldBe(editModel.ClaimSetName); + } + + [Test] + public void ShouldThrowExceptionOnEditSystemReservedClaimSet() + { + var systemReservedClaimSet = new ClaimSet { ClaimSetName = "Ed-Fi Sandbox", IsEdfiPreset = true }; + Save(systemReservedClaimSet); + + var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSetEdited", ClaimSetId = systemReservedClaimSet.ClaimSetId }; + + using var securityContext = TestContext; + var exception = Assert.Throws(() => UsersTransaction(usersContext => + { + var command = new EditClaimSetCommand(TestContext, usersContext); + command.Execute(editModel); + })); + exception.ShouldNotBeNull(); + exception.Message.ShouldBe($"Claim set ({systemReservedClaimSet.ClaimSetName}) is system reserved. May not be modified."); + } + + [Test] + public void ShouldEditClaimSetWithVendorApplications() + { + var claimSetToBeEdited = new ClaimSet { ClaimSetName = $"TestClaimSet{Guid.NewGuid():N}" }; + Save(claimSetToBeEdited); + SetupVendorApplicationsForClaimSet(claimSetToBeEdited); + + var claimSetNotToBeEdited = new ClaimSet { ClaimSetName = $"TestClaimSet{Guid.NewGuid():N}" }; + Save(claimSetNotToBeEdited); + SetupVendorApplicationsForClaimSet(claimSetNotToBeEdited); + + var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSetEdited", ClaimSetId = claimSetToBeEdited.ClaimSetId }; + + using var securityContext = TestContext; + UsersTransaction(usersContext => + { + var command = new EditClaimSetCommand(securityContext, usersContext); + command.Execute(editModel); + }); + + var editedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == claimSetToBeEdited.ClaimSetId); + editedClaimSet.ClaimSetName.ShouldBe(editModel.ClaimSetName); + AssertApplicationsForClaimSet(claimSetToBeEdited.ClaimSetId, editModel.ClaimSetName, securityContext); + + var unEditedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == claimSetNotToBeEdited.ClaimSetId); + unEditedClaimSet.ClaimSetName.ShouldBe(claimSetNotToBeEdited.ClaimSetName); + AssertApplicationsForClaimSet(claimSetNotToBeEdited.ClaimSetId, claimSetNotToBeEdited.ClaimSetName, securityContext); + } + + private void SetupVendorApplicationsForClaimSet(ClaimSet testClaimSet, int applicationCount = 5) + { + UsersTransaction(usersContext => + { + foreach (var _ in Enumerable.Range(1, applicationCount)) + { + usersContext.Applications.Add(new VendorApplication + { + ApplicationName = $"TestAppVendorName{Guid.NewGuid():N}", + ClaimSetName = testClaimSet.ClaimSetName, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }); + } + usersContext.SaveChanges(); + }); + } + + private void AssertApplicationsForClaimSet(int claimSetId, string claimSetNameToAssert, ISecurityContext securityContext) + { + UsersTransaction( + usersContext => + { + var results = new GetApplicationsByClaimSetIdQuery(securityContext, usersContext).Execute(claimSetId); + var testApplications = usersContext.Applications.Where(x => x.ClaimSetName == claimSetNameToAssert).ToList(); + results.Count().ShouldBe(testApplications.Count); + results.Select(x => x.Name).ShouldBe(testApplications.Select(x => x.ApplicationName), true); + }); + } + + // TODO: move these to UnitTests, using appropriate validator from the API project + //[Test] + //public void ShouldNotEditClaimSetIfNameNotUnique() + //{ + // var testApplication = new Application + // { + // ApplicationName = $"Test Application {DateTime.Now:O}" + // }; + // SaveAdminContext(testApplication); + + // var alreadyExistingClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet1", Application = testApplication }; + // Save(alreadyExistingClaimSet); + + // var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet2", Application = testApplication }; + // Save(testClaimSet); + + // var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSet1", ClaimSetId = testClaimSet.ClaimSetId }; + + // using var securityContext = TestContext; + // var validator = new EditClaimSetModelValidator(AllClaimSetsQuery(securityContext), + // ClaimSetByIdQuery(securityContext)); + // var validationResults = validator.Validate(editModel); + // validationResults.IsValid.ShouldBe(false); + // validationResults.Errors.Single().ErrorMessage.ShouldBe("A claim set with this name already exists in the database. Please enter a unique name."); + //} + + //[Test] + //public void ShouldNotEditClaimSetIfNameEmpty() + //{ + // var testApplication = new Application + // { + // ApplicationName = $"Test Application {DateTime.Now:O}" + // }; + // SaveAdminContext(testApplication); + + // var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet1", Application = testApplication }; + // Save(testClaimSet); + + // var editModel = new EditClaimSetModel { ClaimSetName = "", ClaimSetId = testClaimSet.ClaimSetId }; + + // using var securityContext = TestContext; + // var validator = new EditClaimSetModelValidator(AllClaimSetsQuery(securityContext), + // ClaimSetByIdQuery(securityContext)); + // var validationResults = validator.Validate(editModel); + // validationResults.IsValid.ShouldBe(false); + // validationResults.Errors.Single().ErrorMessage.ShouldBe("'Claim Set Name' must not be empty."); + //} + + //[Test] + //public void ShouldNotEditClaimSetIfNameLengthGreaterThan255Characters() + //{ + // var testApplication = new Application + // { + // ApplicationName = $"Test Application {DateTime.Now:O}" + // }; + // SaveAdminContext(testApplication); + + // var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet1", Application = testApplication }; + // Save(testClaimSet); + + // var editModel = new EditClaimSetModel { ClaimSetName = "ThisIsAClaimSetWithNameLengthGreaterThan255CharactersThisIsAClaimSetWithNameLengthGreaterThan255CharactersThisIsAClaimSetWithNameLengthGreaterThan255CharactersThisIsAClaimSetWithNameLengthGreaterThan255CharactersThisIsAClaimSetWithNameLengthGreaterThan255CharactersThisIsAClaimSetWithNameLengthGreaterThan255Characters", ClaimSetId = testClaimSet.ClaimSetId }; + + // using var securityContext = TestContext; + // var validator = new EditClaimSetModelValidator(AllClaimSetsQuery(securityContext), + // ClaimSetByIdQuery(securityContext)); + // var validationResults = validator.Validate(editModel); + // validationResults.IsValid.ShouldBe(false); + // validationResults.Errors.Single().ErrorMessage.ShouldBe("The claim set name must be less than 255 characters."); + //} + + //private GetClaimSetByIdQuery ClaimSetByIdQuery(ISecurityContext securityContext) => new GetClaimSetByIdQuery(new StubOdsSecurityModelVersionResolver.V6(), + // null, new GetClaimSetByIdQueryV6Service(securityContext)); + + //private IGetAllClaimSetsQuery AllClaimSetsQuery(ISecurityContext securityContext) => new GetAllClaimSetsQuery(securityContext); +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandV53ServiceTests.cs deleted file mode 100644 index 8965229e5..000000000 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandV53ServiceTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using System; -using System.Linq; -using NUnit.Framework; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using Shouldly; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using VendorApplication = EdFi.Admin.DataAccess.Models.Application; -using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet; -using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application; -using EdFi.Ods.AdminApi.Features.ClaimSets; -using EdFi.Ods.AdminApi.Infrastructure; - -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; - -[TestFixture] -public class EditClaimSetCommandV53ServiceTests : SecurityData53TestBase -{ - [Test] - public void ShouldEditClaimSet() - { - var testApplication = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(testApplication); - - var alreadyExistingClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication }; - Save(alreadyExistingClaimSet); - - var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSetEdited", ClaimSetId = alreadyExistingClaimSet.ClaimSetId }; - - using var securityContext = TestContext; - UsersTransaction((usersContext) => - { - var command = new EditClaimSetCommandV53Service(securityContext, usersContext); - command.Execute(editModel); - }); - - var editedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == alreadyExistingClaimSet.ClaimSetId); - editedClaimSet.ClaimSetName.ShouldBe(editModel.ClaimSetName); - } - - [Test] - public void ShouldThrowExceptionOnEditSystemReservedClaimSet() - { - var testApplication = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(testApplication); - - var systemReservedClaimSet = new ClaimSet { ClaimSetName = "Ed-Fi Sandbox", Application = testApplication }; - Save(systemReservedClaimSet); - - var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSetEdited", ClaimSetId = systemReservedClaimSet.ClaimSetId }; - - using var securityContext = TestContext; - var exception = Assert.Throws(() => UsersTransaction(usersContext => - { - var command = new EditClaimSetCommandV53Service(securityContext, usersContext); - command.Execute(editModel); - })); - exception.ShouldNotBeNull(); - exception.Message.ShouldBe($"Claim set ({systemReservedClaimSet.ClaimSetName}) is system reserved.May not be modified."); - } - - [Test] - public void ShouldEditClaimSetWithVendorApplications() - { - var testApplication = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(testApplication); - - var claimSetToBeEdited = new ClaimSet { ClaimSetName = $"TestClaimSet{Guid.NewGuid():N}", Application = testApplication }; - Save(claimSetToBeEdited); - SetupVendorApplicationsForClaimSet(claimSetToBeEdited); - - var claimSetNotToBeEdited = new ClaimSet { ClaimSetName = $"TestClaimSet{Guid.NewGuid():N}", Application = testApplication }; - Save(claimSetNotToBeEdited); - SetupVendorApplicationsForClaimSet(claimSetNotToBeEdited); - - var editModel = new EditClaimSetModel { ClaimSetName = "TestClaimSetEdited", ClaimSetId = claimSetToBeEdited.ClaimSetId }; - - using var securityContext = TestContext; - UsersTransaction(usersContext => - { - var command = new EditClaimSetCommandV53Service(securityContext, usersContext); - command.Execute(editModel); - }); - - var editedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == claimSetToBeEdited.ClaimSetId); - editedClaimSet.ClaimSetName.ShouldBe(editModel.ClaimSetName); - AssertApplicationsForClaimSet(claimSetToBeEdited.ClaimSetId, editModel.ClaimSetName, securityContext); - - - var unEditedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == claimSetNotToBeEdited.ClaimSetId); - unEditedClaimSet.ClaimSetName.ShouldBe(claimSetNotToBeEdited.ClaimSetName); - AssertApplicationsForClaimSet(claimSetNotToBeEdited.ClaimSetId, claimSetNotToBeEdited.ClaimSetName, securityContext); - } - - private void SetupVendorApplicationsForClaimSet(ClaimSet testClaimSet, int applicationCount = 5) - { - UsersTransaction(usersContext => - { - foreach (var _ in Enumerable.Range(1, applicationCount)) - { - usersContext.Applications.Add(new VendorApplication - { - ApplicationName = $"TestAppVendorName{Guid.NewGuid():N}", - ClaimSetName = testClaimSet.ClaimSetName, - OperationalContextUri = OperationalContext.DefaultOperationalContextUri - }); - } - usersContext.SaveChanges(); - }); - } - - private void AssertApplicationsForClaimSet(int claimSetId, string claimSetNameToAssert, ISecurityContext securityContext) - { - UsersTransaction( - usersContext => - { - var results = new GetApplicationsByClaimSetId53Query(securityContext, usersContext).Execute(claimSetId); - var testApplications = usersContext.Applications.Where(x => x.ClaimSetName == claimSetNameToAssert).ToList(); - results.Count().ShouldBe(testApplications.Count); - results.Select(x => x.Name).ShouldBe(testApplications.Select(x => x.ApplicationName), true); - }); - } -} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandTests.cs new file mode 100644 index 000000000..2597b1620 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandTests.cs @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using Moq; +using NUnit.Framework; +using Shouldly; +using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; +using ResourceClaim = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaim; + +namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; + +[TestFixture] +public class EditResourceOnClaimSetCommandTests : SecurityDataTestBase +{ + [Test] + public void ShouldEditParentResourcesOnClaimSet() + { + var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" }; + Save(testClaimSet); + + var parentRcNames = UniqueNameList("ParentRc", 2); + var testResources = SetupClaimSetResourceClaimActions(testClaimSet, parentRcNames, UniqueNameList("ChildRc", 1)); + + var testResource1ToEdit = testResources.Select(x => x.ResourceClaim).Single(x => x.ResourceName == parentRcNames.First()); + var testResource2ToNotEdit = testResources.Select(x => x.ResourceClaim).Single(x => x.ResourceName == parentRcNames.Last()); + + var editedResource = new ResourceClaim + { + Id = testResource1ToEdit.ResourceClaimId, + Name = testResource1ToEdit.ResourceName, + Actions = new List + { + new ResourceClaimAction{ Name = "Create", Enabled = false }, + new ResourceClaimAction{ Name = "Read", Enabled = false }, + new ResourceClaimAction{ Name = "Update", Enabled = true }, + new ResourceClaimAction{ Name = "Delete", Enabled = true } + } + }; + + var editResourceOnClaimSetModel = new Mock(); + editResourceOnClaimSetModel.Setup(x => x.ClaimSetId).Returns(testClaimSet.ClaimSetId); + editResourceOnClaimSetModel.Setup(x => x.ResourceClaim).Returns(editedResource); + + using var securityContext = TestContext; + var command = new EditResourceOnClaimSetCommand(securityContext); + command.Execute(editResourceOnClaimSetModel.Object); + + var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); + + var resultResourceClaim1 = resourceClaimsForClaimSet.Single(x => x.Id == editedResource.Id); + + resultResourceClaim1.Actions.ShouldNotBeNull(); + resultResourceClaim1.Actions.Count.ShouldBe(2); + resultResourceClaim1.Actions.All(x => + editedResource.Actions.Any(y => x.Name.Equals(y.Name) && x.Enabled.Equals(y.Enabled))).ShouldBe(true); + + var resultResourceClaim2 = resourceClaimsForClaimSet.Single(x => x.Id == testResource2ToNotEdit.ResourceClaimId); + resultResourceClaim2.Actions.ShouldNotBeNull(); + resultResourceClaim2.Actions.Count.ShouldBe(1); + resultResourceClaim2.Actions.Any(x => x.Name.Equals("Create")).ShouldBe(true); + } + + [Test] + public void ShouldEditChildResourcesOnClaimSet() + { + var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" }; + Save(testClaimSet); + + var parentRcNames = UniqueNameList("ParentRc", 1); + var childRcNames = UniqueNameList("ChildRc", 2); + var testResources = SetupClaimSetResourceClaimActions(testClaimSet, parentRcNames, childRcNames); + + var testParentResource = testResources.Single(x => x.ResourceClaim.ResourceName == parentRcNames.First()); + + var test1ChildResourceClaim = $"{childRcNames.First()}-{parentRcNames.First()}"; + var test2ChildResourceClaim = $"{childRcNames.Last()}-{parentRcNames.First()}"; + + using var securityContext = TestContext; + var testChildResource1ToEdit = securityContext.ResourceClaims.Single(x => x.ResourceName == test1ChildResourceClaim && x.ParentResourceClaimId == testParentResource.ResourceClaim.ResourceClaimId); + var testChildResource2NotToEdit = securityContext.ResourceClaims.Single(x => x.ResourceName == test2ChildResourceClaim && x.ParentResourceClaimId == testParentResource.ResourceClaim.ResourceClaimId); + var editedResource = new ResourceClaim + { + Id = testChildResource1ToEdit.ResourceClaimId, + Name = testChildResource1ToEdit.ResourceName, + Actions = new List + { + new ResourceClaimAction{ Name = "Create", Enabled = false }, + new ResourceClaimAction{ Name = "Read", Enabled = false }, + new ResourceClaimAction{ Name = "Update", Enabled = true }, + new ResourceClaimAction{ Name = "Delete", Enabled = true } + } + }; + + var editResourceOnClaimSetModel = new Mock(); + editResourceOnClaimSetModel.Setup(x => x.ClaimSetId).Returns(testClaimSet.ClaimSetId); + editResourceOnClaimSetModel.Setup(x => x.ResourceClaim).Returns(editedResource); + + var command = new EditResourceOnClaimSetCommand(securityContext); + command.Execute(editResourceOnClaimSetModel.Object); + + var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); + + var resultParentResourceClaim = resourceClaimsForClaimSet.Single(x => x.Id == testParentResource.ResourceClaim.ResourceClaimId); + resultParentResourceClaim.Actions.ShouldNotBeNull(); + resultParentResourceClaim.Actions.Count.ShouldBe(1); + resultParentResourceClaim.Actions.Any(x => x.Name.Equals("Create")).ShouldBe(true); + + var resultChildResourceClaim1 = + resultParentResourceClaim.Children.Single(x => x.Id == editedResource.Id); + + resultChildResourceClaim1.Actions.ShouldNotBeNull(); + resultChildResourceClaim1.Actions.Count.ShouldBe(2); + resultChildResourceClaim1.Actions.All(x => + editedResource.Actions.Any(y => x.Name.Equals(y.Name) && x.Enabled.Equals(y.Enabled))).ShouldBe(true); + + var resultChildResourceClaim2 = + resultParentResourceClaim.Children.Single(x => x.Id == testChildResource2NotToEdit.ResourceClaimId); + + resultParentResourceClaim.Actions.ShouldNotBeNull(); + resultParentResourceClaim.Actions.Count.ShouldBe(1); + resultParentResourceClaim.Actions.Any(x => x.Name.Equals("Create")).ShouldBe(true); + } + + [Test] + public void ShouldEditGrandChildResourcesOnClaimSet() + { + var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" }; + Save(testClaimSet); + + var grandChildRcNamePrefix = "GrandChildRc"; + + var parentRcNames = UniqueNameList("ParentRc", 1); + var childRcNames = UniqueNameList("ChildRc", 2); + var grandChildRcNames = UniqueNameList(grandChildRcNamePrefix, 1); + var testResources = SetupClaimSetResourceClaimActions(testClaimSet, parentRcNames, childRcNames, grandChildRcNames); + + var testParentResource = testResources.Single(x => x.ResourceClaim.ResourceName == parentRcNames.First()); + + var test1ChildResourceClaim = $"{childRcNames.First()}-{parentRcNames.First()}"; + var test2ChildResourceClaim = $"{childRcNames.Last()}-{parentRcNames.First()}"; + + using var securityContext = TestContext; + + var grandChildResources = securityContext.ResourceClaims.Where(rc => rc.ClaimName.StartsWith(grandChildRcNamePrefix)).ToArray(); + + // there must be at least two grandchild ResourceClaims so that one can be edited while the other remains unchanged + grandChildResources.ShouldNotBeNull(); + grandChildResources.Count().ShouldBeGreaterThanOrEqualTo(2); + + var grandChildRCToEdit = grandChildResources[0]; + var grandChildRCNotToEdit = grandChildResources[1]; + + var editedResource = new ResourceClaim + { + Id = grandChildRCToEdit.ResourceClaimId, + Name = grandChildRCToEdit.ResourceName, + Actions = new List + { + new ResourceClaimAction{ Name = "Create", Enabled = false }, + new ResourceClaimAction{ Name = "Read", Enabled = false }, + new ResourceClaimAction{ Name = "Update", Enabled = true }, + new ResourceClaimAction{ Name = "Delete", Enabled = true } + } + }; + + var editResourceOnClaimSetModel = new Mock(); + editResourceOnClaimSetModel.Setup(x => x.ClaimSetId).Returns(testClaimSet.ClaimSetId); + editResourceOnClaimSetModel.Setup(x => x.ResourceClaim).Returns(editedResource); + + var command = new EditResourceOnClaimSetCommand(securityContext); + command.Execute(editResourceOnClaimSetModel.Object); + + + var resourceClaims = securityContext.ClaimSetResourceClaimActions.Where(rca => rca.ClaimSetId == testClaimSet.ClaimSetId); + + + var editedGrandChildResourceClaimAction = resourceClaims.Where(rca => rca.ResourceClaimId == grandChildRCToEdit.ResourceClaimId).ToList(); + + editedGrandChildResourceClaimAction.ShouldNotBeNull(); + editedGrandChildResourceClaimAction.Count.ShouldBe(2); + editedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Update")).ShouldBe(true); + editedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Delete")).ShouldBe(true); + editedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Create")).ShouldBe(false); + editedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Read")).ShouldBe(false); + + + var notEditedGrandChildResourceClaimAction = resourceClaims.Where(rca => rca.ResourceClaimId == grandChildRCNotToEdit.ResourceClaimId).ToList(); + + notEditedGrandChildResourceClaimAction.ShouldNotBeNull(); + notEditedGrandChildResourceClaimAction.Count.ShouldBe(1); + notEditedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Create")).ShouldBe(true); + notEditedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Read")).ShouldBe(false); + notEditedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Update")).ShouldBe(false); + notEditedGrandChildResourceClaimAction.Any(rca => rca.Action.ActionName.Equals("Delete")).ShouldBe(false); + + } + + + [Test] + public void ShouldAddParentResourceToClaimSet() + { + var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" }; + Save(testClaimSet); + + var parentRcNames = UniqueNameList("Parent", 1); + var testResources = SetupResourceClaims(parentRcNames, UniqueNameList("child", 1)); + var testResourceToAdd = testResources.Single(x => x.ResourceName == parentRcNames.First()); + var resourceToAdd = new ResourceClaim() + { + Id = testResourceToAdd.ResourceClaimId, + Name = testResourceToAdd.ResourceName, + Actions = new List + { + new ResourceClaimAction{ Name = "Create", Enabled = true }, + new ResourceClaimAction{ Name = "Read", Enabled = false }, + new ResourceClaimAction{ Name = "Update", Enabled = true }, + new ResourceClaimAction{ Name = "Delete", Enabled = false } + } + }; + var existingResources = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); + + var editResourceOnClaimSetModel = new EditResourceOnClaimSetModel + { + ClaimSetId = testClaimSet.ClaimSetId, + ResourceClaim = resourceToAdd + }; + + using var securityContext = TestContext; + var command = new EditResourceOnClaimSetCommand(securityContext); + command.Execute(editResourceOnClaimSetModel); + + var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); + + var resultResourceClaim1 = resourceClaimsForClaimSet.Single(x => x.Name == testResourceToAdd.ResourceName); + + resultResourceClaim1.Actions.ShouldNotBeNull(); + resultResourceClaim1.Actions.Count.ShouldBe(2); + resultResourceClaim1.Actions.All(x => + resourceToAdd.Actions.Any(y => x.Name.Equals(y.Name) && x.Enabled.Equals(y.Enabled))).ShouldBe(true); + } + + [Test] + public void ShouldAddChildResourcesToClaimSet() + { + var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" }; + Save(testClaimSet); + + var parentRcNames = UniqueNameList("Parent", 1); + var childRcNames = UniqueNameList("Child", 1); + var testResources = SetupResourceClaims(parentRcNames, childRcNames); + + var testParentResource1 = testResources.Single(x => x.ResourceName == parentRcNames.First()); + var childRcToTest = $"{childRcNames.First()}-{parentRcNames.First()}"; + + using var securityContext = TestContext; + var testChildResource1ToAdd = securityContext.ResourceClaims.Single(x => x.ResourceName == childRcToTest && x.ParentResourceClaimId == testParentResource1.ResourceClaimId); + var resourceToAdd = new ResourceClaim() + { + Id = testChildResource1ToAdd.ResourceClaimId, + Name = testChildResource1ToAdd.ResourceName, + Actions = new List + { + new ResourceClaimAction{ Name = "Create", Enabled = true }, + new ResourceClaimAction{ Name = "Read", Enabled = false }, + new ResourceClaimAction{ Name = "Update", Enabled = true }, + new ResourceClaimAction{ Name = "Delete", Enabled = false } + } + }; + var existingResources = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); + + var editResourceOnClaimSetModel = new EditResourceOnClaimSetModel + { + ClaimSetId = testClaimSet.ClaimSetId, + ResourceClaim = resourceToAdd + }; + + var command = new EditResourceOnClaimSetCommand(securityContext); + command.Execute(editResourceOnClaimSetModel); + + var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); + + var resultChildResourceClaim1 = + resourceClaimsForClaimSet.Single(x => x.Name == testChildResource1ToAdd.ResourceName); + + resultChildResourceClaim1.Actions.ShouldNotBeNull(); + resultChildResourceClaim1.Actions.Count.ShouldBe(2); + resultChildResourceClaim1.Actions.All(x => + resourceToAdd.Actions.Any(y => x.Name.Equals(y.Name) && x.Enabled.Equals(y.Enabled))).ShouldBe(true); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandV53ServiceTests.cs deleted file mode 100644 index 7394caf57..000000000 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandV53ServiceTests.cs +++ /dev/null @@ -1,227 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using System; -using System.Linq; -using NUnit.Framework; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using Shouldly; -using Moq; -using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application; -using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet; -using ResourceClaim = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaim; - -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; - -[TestFixture] -public class EditResourceOnClaimSetCommandV53ServiceTests : SecurityData53TestBase -{ - [Test] - public void ShouldEditParentResourcesOnClaimSet() - { - var testApplication = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(testApplication); - - var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication }; - Save(testClaimSet); - - var testResources = SetupParentResourceClaimsWithChildren(testClaimSet, testApplication); - - var testResource1ToEdit = testResources.Select(x => x.ResourceClaim).Single(x => x.ResourceName == "TestParentResourceClaim1"); - var testResource2ToNotEdit = testResources.Select(x => x.ResourceClaim).Single(x => x.ResourceName == "TestParentResourceClaim2"); - - var editedResource = new ResourceClaim - { - Id = testResource1ToEdit.ResourceClaimId, - Name = testResource1ToEdit.ResourceName, - Create = false, - Read = false, - Update = true, - Delete = true - }; - - var editResourceOnClaimSetModel = new Mock(); - editResourceOnClaimSetModel.Setup(x => x.ClaimSetId).Returns(testClaimSet.ClaimSetId); - editResourceOnClaimSetModel.Setup(x => x.ResourceClaim).Returns(editedResource); - - using var securityContext = TestContext; - var command = new EditResourceOnClaimSetCommandV53Service(securityContext); - command.Execute(editResourceOnClaimSetModel.Object); - - var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); - - var resultResourceClaim1 = resourceClaimsForClaimSet.Single(x => x.Id == editedResource.Id); - - resultResourceClaim1.Create.ShouldBe(editedResource.Create); - resultResourceClaim1.Read.ShouldBe(editedResource.Read); - resultResourceClaim1.Update.ShouldBe(editedResource.Update); - resultResourceClaim1.Delete.ShouldBe(editedResource.Delete); - - var resultResourceClaim2 = resourceClaimsForClaimSet.Single(x => x.Id == testResource2ToNotEdit.ResourceClaimId); - - resultResourceClaim2.Create.ShouldBe(true); - resultResourceClaim2.Read.ShouldBe(false); - resultResourceClaim2.Update.ShouldBe(false); - resultResourceClaim2.Delete.ShouldBe(false); - } - - [Test] - public void ShouldEditChildResourcesOnClaimSet() - { - var testApplication = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(testApplication); - - var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication }; - Save(testClaimSet); - - var testResources = SetupParentResourceClaimsWithChildren(testClaimSet, testApplication); - - var testParentResource = testResources.Single(x => x.ResourceClaim.ResourceName == "TestParentResourceClaim1"); - - using var securityContext = TestContext; - var testChildResource1ToEdit = securityContext.ResourceClaims.Single(x => x.ResourceName == "TestChildResourceClaim1" && x.ParentResourceClaimId == testParentResource.ResourceClaim.ResourceClaimId); - var testChildResource2NotToEdit = securityContext.ResourceClaims.Single(x => x.ResourceName == "TestChildResourceClaim2" && x.ParentResourceClaimId == testParentResource.ResourceClaim.ResourceClaimId); - - var editedResource = new ResourceClaim - { - Id = testChildResource1ToEdit.ResourceClaimId, - Name = testChildResource1ToEdit.ResourceName, - Create = false, - Read = false, - Update = true, - Delete = true - }; - - var editResourceOnClaimSetModel = new Mock(); - editResourceOnClaimSetModel.Setup(x => x.ClaimSetId).Returns(testClaimSet.ClaimSetId); - editResourceOnClaimSetModel.Setup(x => x.ResourceClaim).Returns(editedResource); - - var command = new EditResourceOnClaimSetCommandV53Service(securityContext); - command.Execute(editResourceOnClaimSetModel.Object); - - var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); - - var resultParentResourceClaim = resourceClaimsForClaimSet.Single(x => x.Id == testParentResource.ResourceClaim.ResourceClaimId); - resultParentResourceClaim.Create.ShouldBe(true); - resultParentResourceClaim.Read.ShouldBe(false); - resultParentResourceClaim.Update.ShouldBe(false); - resultParentResourceClaim.Delete.ShouldBe(false); - - var resultChildResourceClaim1 = - resultParentResourceClaim.Children.Single(x => x.Id == editedResource.Id); - - resultChildResourceClaim1.Create.ShouldBe(editedResource.Create); - resultChildResourceClaim1.Read.ShouldBe(editedResource.Read); - resultChildResourceClaim1.Update.ShouldBe(editedResource.Update); - resultChildResourceClaim1.Delete.ShouldBe(editedResource.Delete); - - var resultChildResourceClaim2 = - resultParentResourceClaim.Children.Single(x => x.Id == testChildResource2NotToEdit.ResourceClaimId); - - resultChildResourceClaim2.Create.ShouldBe(true); - resultChildResourceClaim2.Read.ShouldBe(false); - resultChildResourceClaim2.Update.ShouldBe(false); - resultChildResourceClaim2.Delete.ShouldBe(false); - } - - [Test] - public void ShouldAddParentResourceToClaimSet() - { - var testApplication = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(testApplication); - - var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication }; - Save(testClaimSet); - - var testResources = SetupResourceClaims(testApplication); - var testResourceToAdd = testResources.Single(x => x.ResourceName == "TestParentResourceClaim1"); - var resourceToAdd = new ResourceClaim() - { - Id = testResourceToAdd.ResourceClaimId, - Name = testResourceToAdd.ResourceName, - Create = true, - Read = false, - Update = true, - Delete = false - }; - var existingResources = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); - - var editResourceOnClaimSetModel = new EditResourceOnClaimSetModel - { - ClaimSetId = testClaimSet.ClaimSetId, - ResourceClaim = resourceToAdd - }; - - using var securityContext = TestContext; - var command = new EditResourceOnClaimSetCommandV53Service(securityContext); - command.Execute(editResourceOnClaimSetModel); - - var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); - - var resultResourceClaim1 = resourceClaimsForClaimSet.Single(x => x.Name == testResourceToAdd.ResourceName); - - resultResourceClaim1.Create.ShouldBe(resourceToAdd.Create); - resultResourceClaim1.Read.ShouldBe(resourceToAdd.Read); - resultResourceClaim1.Update.ShouldBe(resourceToAdd.Update); - resultResourceClaim1.Delete.ShouldBe(resourceToAdd.Delete); - } - - [Test] - public void ShouldAddChildResourcesToClaimSet() - { - var testApplication = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(testApplication); - - var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication }; - Save(testClaimSet); - - var testResources = SetupResourceClaims(testApplication); - var testParentResource1 = testResources.Single(x => x.ResourceName == "TestParentResourceClaim1"); - - using var securityContext = TestContext; - var testChildResource1ToAdd = securityContext.ResourceClaims.Single(x => x.ResourceName == "TestChildResourceClaim1" && x.ParentResourceClaimId == testParentResource1.ResourceClaimId); - var resourceToAdd = new ResourceClaim() - { - Id = testChildResource1ToAdd.ResourceClaimId, - Name = testChildResource1ToAdd.ResourceName, - Create = true, - Read = false, - Update = true, - Delete = false - }; - var existingResources = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); - - var editResourceOnClaimSetModel = new EditResourceOnClaimSetModel - { - ClaimSetId = testClaimSet.ClaimSetId, - ResourceClaim = resourceToAdd - }; - var command = new EditResourceOnClaimSetCommandV53Service(securityContext); - command.Execute(editResourceOnClaimSetModel); - - var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); - - var resultChildResourceClaim1 = - resourceClaimsForClaimSet.Single(x => x.Name == testChildResource1ToAdd.ResourceName); - - resultChildResourceClaim1.Create.ShouldBe(resourceToAdd.Create); - resultChildResourceClaim1.Read.ShouldBe(resourceToAdd.Read); - resultChildResourceClaim1.Update.ShouldBe(resourceToAdd.Update); - resultChildResourceClaim1.Delete.ShouldBe(resourceToAdd.Delete); - } -} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetAllClaimSetsQueryV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetAllClaimSetsQueryV53ServiceTests.cs deleted file mode 100644 index f5e122e13..000000000 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetAllClaimSetsQueryV53ServiceTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using System; -using System.Linq; -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; -using NUnit.Framework; -using Shouldly; - -using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet; -using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application; -using EdFi.Ods.AdminApi.Infrastructure; - -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; - -[TestFixture] -public class GetAllClaimSetsQueryV53ServiceTests : SecurityData53TestBase -{ - public GetAllClaimSetsQueryV53ServiceTests() - { - SeedSecurityContextOnFixtureSetup = true; - } - - [Test] - public void Should_Retrieve_ClaimSetNames() - { - var application = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(application); - - var claimSet1 = GetClaimSet(application); - var claimSet2 = GetClaimSet(application); - Save(claimSet1, claimSet2); - - var claimSetNames = Transaction(securityContext => - { - var query = new GetAllClaimSetsQueryV53Service(securityContext, Testing.GetAppSettings()); - return query.Execute().Select(x => x.Name).ToArray(); - }); - - claimSetNames.ShouldContain(claimSet1.ClaimSetName); - claimSetNames.ShouldContain(claimSet2.ClaimSetName); - } - - [Test] - public void Should_Retrieve_ClaimSetNames_with_offset_and_limit() - { - var application = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(application); - - var claimSet1 = GetClaimSet(application); - var claimSet2 = GetClaimSet(application); - var claimSet3 = GetClaimSet(application); - var claimSet4 = GetClaimSet(application); - var claimSet5 = GetClaimSet(application); - - Save(claimSet1, claimSet2, claimSet3, claimSet4, claimSet5); - - var commonQueryParams = new CommonQueryParams(0, 2); - var claimSetNames = Transaction(securityContext => - { - var query = new GetAllClaimSetsQueryV53Service(securityContext, Testing.GetAppSettings()); - return query.Execute(commonQueryParams).Select(x => x.Name).ToArray(); - }); - - claimSetNames.Length.ShouldBe(2); - claimSetNames.ShouldContain(claimSet1.ClaimSetName); - claimSetNames.ShouldContain(claimSet2.ClaimSetName); - - commonQueryParams.Offset = 2; - claimSetNames = Transaction(securityContext => - { - var query = new GetAllClaimSetsQueryV53Service(securityContext, Testing.GetAppSettings()); - return query.Execute(commonQueryParams).Select(x => x.Name).ToArray(); - }); - - claimSetNames.Length.ShouldBe(2); - claimSetNames.ShouldContain(claimSet3.ClaimSetName); - claimSetNames.ShouldContain(claimSet4.ClaimSetName); - - commonQueryParams.Offset = 4; - claimSetNames = Transaction(securityContext => - { - var query = new GetAllClaimSetsQueryV53Service(securityContext, Testing.GetAppSettings()); - return query.Execute(commonQueryParams).Select(x => x.Name).ToArray(); - }); - - claimSetNames.Length.ShouldBe(1); - claimSetNames.ShouldContain(claimSet5.ClaimSetName); - } - - private static int _claimSetId = 0; - private static ClaimSet GetClaimSet(Application application) - { - return new ClaimSet - { - Application = application, - ClaimSetName = $"Test Claim Set {_claimSetId++} - {DateTime.Now:O}" - }; - } -} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryV6ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryTests.cs similarity index 91% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryV6ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryTests.cs index 5fc304524..2ade65c63 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryV6ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryTests.cs @@ -9,15 +9,16 @@ using NUnit.Framework; using Shouldly; using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using Application = EdFi.Security.DataAccess.Models.Application; +using Application = EdFi.Admin.DataAccess.Models.Application; using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; using VendorApplication = EdFi.Admin.DataAccess.Models.Application; using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure; namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; [TestFixture] -public class GetApplicationsByClaimSetIdQueryV6ServiceTests : SecurityDataTestBase +public class GetApplicationsByClaimSetIdQueryTests : SecurityDataTestBase { [TestCase(1)] [TestCase(3)] @@ -74,13 +75,6 @@ public void ShouldGetClaimSetApplicationsCount(int applicationsCount) private IReadOnlyCollection SetupApplicationWithClaimSets( string applicationName = "TestApplicationName", int claimSetCount = 5) { - var testApplication = new Application - { - ApplicationName = applicationName - }; - - Save(testApplication); - var testClaimSetNames = Enumerable.Range(1, claimSetCount) .Select((x, index) => $"TestClaimSetName{index:N}") .ToArray(); @@ -88,8 +82,7 @@ private IReadOnlyCollection SetupApplicationWithClaimSets( var testClaimSets = testClaimSetNames .Select(x => new ClaimSet { - ClaimSetName = x, - Application = testApplication + ClaimSetName = x }) .ToArray(); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryTests.cs new file mode 100644 index 000000000..f4876194f --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryTests.cs @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Security.DataAccess.Contexts; +using NUnit.Framework; +using Shouldly; +using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; + +namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; + +[TestFixture] +public class GetClaimSetByIdQueryTests : SecurityDataTestBase +{ + [Test] + public void ShouldGetClaimSetById() + { + var testClaimSet = new ClaimSet + { + ClaimSetName = "TestClaimSet", + ForApplicationUseOnly = false, + IsEdfiPreset = false + }; + Save(testClaimSet); + + using var securityContext = TestContext; + var query = new GetClaimSetByIdQuery(securityContext); + var result = query.Execute(testClaimSet.ClaimSetId); + result.Name.ShouldBe(testClaimSet.ClaimSetName); + result.Id.ShouldBe(testClaimSet.ClaimSetId); + result.IsEditable.ShouldBe(true); + } + + [Test] + public void ShouldGetNonEditableClaimSetById() + { + var systemReservedClaimSet = new ClaimSet + { + ClaimSetName = "SystemReservedClaimSet", + ForApplicationUseOnly = true + }; + Save(systemReservedClaimSet); + + var edfiPresetClaimSet = new ClaimSet + { + ClaimSetName = "EdfiPresetClaimSet", + ForApplicationUseOnly = false, + IsEdfiPreset = true + }; + Save(edfiPresetClaimSet); + + using var securityContext = TestContext; + var query = new GetClaimSetByIdQuery(securityContext); + var result = query.Execute(systemReservedClaimSet.ClaimSetId); + result.Name.ShouldBe(systemReservedClaimSet.ClaimSetName); + result.Id.ShouldBe(systemReservedClaimSet.ClaimSetId); + result.IsEditable.ShouldBe(false); + + result = query.Execute(edfiPresetClaimSet.ClaimSetId); + + result.Name.ShouldBe(edfiPresetClaimSet.ClaimSetName); + result.Id.ShouldBe(edfiPresetClaimSet.ClaimSetId); + result.IsEditable.ShouldBe(false); + } + + [Test] + public void ShouldThrowExceptionForNonExistingClaimSetId() + { + const int NonExistingClaimSetId = 1; + + using var securityContext = TestContext; + EnsureZeroClaimSets(securityContext); + + var exception = Assert.Throws>(() => + { + var query = new GetClaimSetByIdQuery(securityContext); + query.Execute(NonExistingClaimSetId); + }); + exception.ShouldNotBeNull(); + exception.Message.ShouldBe("Not found: claimset with ID 1. It may have been recently deleted."); + + static void EnsureZeroClaimSets(ISecurityContext database) + { + foreach (var entity in database.ClaimSets) + database.ClaimSets.Remove(entity); + database.SaveChanges(); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryV53ServiceTests.cs deleted file mode 100644 index 6b48973d4..000000000 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryV53ServiceTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using System; -using NUnit.Framework; -using Shouldly; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using System.Net; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; - -using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet; -using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application; - -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; - -[TestFixture] -public class GetClaimSetByIdQueryV53ServiceTests : SecurityData53TestBase -{ - [Test] - public void ShouldGetClaimSetById() - { - var testApplication = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(testApplication); - - var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication }; - Save(testClaimSet); - - using var securityContext = TestContext; - var query = new GetClaimSetByIdQueryV53Service(securityContext); - var result = query.Execute(testClaimSet.ClaimSetId); - result.Name.ShouldBe(testClaimSet.ClaimSetName); - result.Id.ShouldBe(testClaimSet.ClaimSetId); - } - - [Test] - public void ShouldThrowExceptionForNonExistingClaimSetId() - { - const int NonExistingClaimSetId = 1; - - using var securityContext = TestContext; - EnsureZeroClaimSets(securityContext); - var adminApiException = Assert.Throws(() => - { - var query = new GetClaimSetByIdQueryV53Service(securityContext); - query.Execute(NonExistingClaimSetId); - }); - adminApiException.ShouldNotBeNull(); - adminApiException.StatusCode.ShouldBe(HttpStatusCode.NotFound); - adminApiException.Message.ShouldBe("No such claim set exists in the database."); - - static void EnsureZeroClaimSets(ISecurityContext database) - { - foreach (var entity in database.ClaimSets) - database.ClaimSets.Remove(entity); - database.SaveChanges(); - } - } -} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryTests.cs similarity index 67% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryV53ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryTests.cs index eaa76f3f1..1833b5959 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryV53ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryTests.cs @@ -2,101 +2,82 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; -using System.Collections.Generic; -using System.Linq; +using EdFi.Security.DataAccess.Models; using NUnit.Framework; using Shouldly; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models; - -using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application; -using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet; -using ResourceClaim = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ResourceClaim; -using Action = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Action; +using System.Collections.Generic; +using System.Linq; +using Action = EdFi.Security.DataAccess.Models.Action; using ActionName = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.Action; +using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; +using ResourceClaim = EdFi.Security.DataAccess.Models.ResourceClaim; namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; [TestFixture] -public class GetResourcesByClaimSetIdQueryV53ServiceTests : SecurityData53TestBase +public class GetResourcesByClaimSetIdQueryTests : SecurityDataTestBase { [Test] public void ShouldGetParentResourcesByClaimSetId() { - var testApplication = new Application - { - ApplicationName = "TestApplicationName" - }; + var testClaimSets = SetupApplicationWithClaimSets().ToList(); + var testResourceClaims = SetupParentResourceClaims(testClaimSets, UniqueNameList("ParentRc", 1)); - Save(testApplication); - var testClaimSets = SetupApplicationWithClaimSets(testApplication).ToList(); - var testResourceClaims = SetupParentResourceClaims(testClaimSets, testApplication); foreach (var testClaimSet in testClaimSets) { - var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); + var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId).ToArray(); var testResourceClaimsForId = testResourceClaims.Where(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId).Select(x => x.ResourceClaim).ToArray(); - results.Count.ShouldBe(testResourceClaimsForId.Length); + results.Length.ShouldBe(testResourceClaimsForId.Length); results.Select(x => x.Name).ShouldBe(testResourceClaimsForId.Select(x => x.ResourceName), true); results.Select(x => x.Id).ShouldBe(testResourceClaimsForId.Select(x => x.ResourceClaimId), true); - results.All(x => x.Create).ShouldBe(true); + results.All(x => x.Actions.All(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true); } } [Test] public void ShouldGetSingleResourceByClaimSetIdAndResourceId() { - var testApplication = new Application - { - ApplicationName = "TestApplicationName" - }; - - Save(testApplication); - var testClaimSets = SetupApplicationWithClaimSets(testApplication).ToList(); - var testResourceClaims = SetupParentResourceClaims(testClaimSets, testApplication); + var testClaimSets = SetupApplicationWithClaimSets().ToList(); + var rcIds = UniqueNameList("ParentRc", 1); + var testResourceClaims = SetupParentResourceClaims(testClaimSets, rcIds); foreach (var testClaimSet in testClaimSets) { + var rcName = $"{rcIds.First()}{testClaimSet.ClaimSetName}"; var testResourceClaim = - testResourceClaims.Single(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId && x.ResourceClaim.ResourceName == "TestResourceClaim3.00").ResourceClaim; + testResourceClaims.Single(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId && x.ResourceClaim.ResourceName == rcName).ResourceClaim; var result = SingleResourceClaimForClaimSet(testClaimSet.ClaimSetId, testResourceClaim.ResourceClaimId); result.Name.ShouldBe(testResourceClaim.ResourceName); result.Id.ShouldBe(testResourceClaim.ResourceClaimId); - result.Create.ShouldBe(true); - result.Read.ShouldBe(false); - result.Update.ShouldBe(false); - result.Delete.ShouldBe(false); + result.Actions.All(x => x.Name.Equals("Create") && x.Enabled).ShouldBe(true); + result.Actions.All(x => x.Name.Equals("Read") && x.Enabled).ShouldBe(false); + result.Actions.All(x => x.Name.Equals("Update") && x.Enabled).ShouldBe(false); + result.Actions.All(x => x.Name.Equals("Delete") && x.Enabled).ShouldBe(false); } - } [Test] public void ShouldGetParentResourcesWithChildrenByClaimSetId() { - var testApplication = new Application - { - ApplicationName = "TestApplicationName" - }; - - Save(testApplication); - var testClaimSets = SetupApplicationWithClaimSets(testApplication); - var testResourceClaims = SetupParentResourceClaimsWithChildren(testClaimSets, testApplication); + var testClaimSets = SetupApplicationWithClaimSets(); + var testResourceClaims = SetupParentResourceClaimsWithChildren(testClaimSets); using var securityContext = TestContext; foreach (var testClaimSet in testClaimSets) { - var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); + var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId).ToArray(); var testParentResourceClaimsForId = testResourceClaims.Where(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId && x.ResourceClaim.ParentResourceClaim == null).Select(x => x.ResourceClaim).ToArray(); - results.Count.ShouldBe(testParentResourceClaimsForId.Length); + results.Length.ShouldBe(testParentResourceClaimsForId.Length); results.Select(x => x.Name).ShouldBe(testParentResourceClaimsForId.Select(x => x.ResourceName), true); results.Select(x => x.Id).ShouldBe(testParentResourceClaimsForId.Select(x => x.ResourceClaimId), true); - results.All(x => x.Create).ShouldBe(true); + results.All(x => x.Actions.All(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true); foreach (var testParentResourceClaim in testParentResourceClaimsForId) { @@ -105,93 +86,78 @@ public void ShouldGetParentResourcesWithChildrenByClaimSetId() var parentResult = results.First(x => x.Id == testParentResourceClaim.ResourceClaimId); parentResult.Children.Select(x => x.Name).ShouldBe(testChildren.Select(x => x.ResourceName), true); parentResult.Children.Select(x => x.Id).ShouldBe(testChildren.Select(x => x.ResourceClaimId), true); - parentResult.Children.All(x => x.Create).ShouldBe(true); + parentResult.Children.All(x => x.Actions.All(x => x.Name.Equals("Create") && x.Enabled)).ShouldBe(true); } + } } [Test] public void ShouldGetDefaultAuthorizationStrategiesForParentResourcesByClaimSetId() { - var testApplication = new Application - { - ApplicationName = "TestApplicationName" - }; - Save(testApplication); - var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", - Application = testApplication }; Save(testClaimSet); - var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies(testApplication).ToList(); - var testResourceClaims = SetupParentResourceClaims(new List { testClaimSet }, testApplication); + var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies().ToList(); + var testResourceClaims = SetupParentResourceClaims(new List { testClaimSet }, UniqueNameList("ParentRc", 3)); var testAuthStrategies = SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, testResourceClaims.ToList()); + var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId).ToArray(); - results.Select(x => x.DefaultAuthStrategiesForCRUD[0].AuthorizationStrategies[0].AuthStrategyName).ShouldBe(testAuthStrategies.Select(x => x.AuthorizationStrategy.AuthorizationStrategyName), true); + results.Select(x => x.DefaultAuthorizationStrategiesForCRUD[0].AuthorizationStrategies.ToList().First().AuthStrategyName).ShouldBe(testAuthStrategies.Select(x => x.AuthorizationStrategies.Single() + .AuthorizationStrategy.AuthorizationStrategyName), true); } [Test] public void ShouldGetDefaultAuthorizationStrategiesForSingleResourcesByClaimSetIdAndResourceId() { - var testApplication = new Application - { - ApplicationName = "TestApplicationName" - }; - Save(testApplication); - var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", - Application = testApplication }; Save(testClaimSet); - var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies(testApplication).ToList(); - var testResourceClaims = SetupParentResourceClaims(new List { testClaimSet }, testApplication); + var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies().ToList(); + var rcIds = UniqueNameList("Parent", 1); + var testResourceClaims = SetupParentResourceClaims(new List { testClaimSet }, rcIds); var testAuthStrategies = SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, testResourceClaims.ToList()); + var rcName = $"{rcIds.First()}{testClaimSet.ClaimSetName}"; var testResourceClaim = - testResourceClaims.Single(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId && x.ResourceClaim.ResourceName == "TestResourceClaim3.00").ResourceClaim; + testResourceClaims.Single(x => x.ClaimSet.ClaimSetId == testClaimSet.ClaimSetId && x.ResourceClaim.ResourceName == rcName).ResourceClaim; var testAuthStrategy = testAuthStrategies.Single(x => - x.ResourceClaim.ResourceClaimId == testResourceClaim.ResourceClaimId && x.Action.ActionName == ActionName.Create.Value).AuthorizationStrategy; + x.ResourceClaim.ResourceClaimId == testResourceClaim.ResourceClaimId && x.Action.ActionName == ActionName.Create.Value) + .AuthorizationStrategies.Single().AuthorizationStrategy; var result = SingleResourceClaimForClaimSet(testClaimSet.ClaimSetId, testResourceClaim.ResourceClaimId); result.Name.ShouldBe(testResourceClaim.ResourceName); result.Id.ShouldBe(testResourceClaim.ResourceClaimId); - result.Create.ShouldBe(true); - result.Read.ShouldBe(false); - result.Update.ShouldBe(false); - result.Delete.ShouldBe(false); - result.DefaultAuthStrategiesForCRUD[0].AuthorizationStrategies[0].AuthStrategyName.ShouldBe(testAuthStrategy.DisplayName); + result.Actions.All(x => x.Name.Equals("Create") && x.Enabled).ShouldBe(true); + result.Actions.All(x => x.Name.Equals("Read") && x.Enabled).ShouldBe(false); + result.Actions.All(x => x.Name.Equals("Update") && x.Enabled).ShouldBe(false); + result.Actions.All(x => x.Name.Equals("Delete") && x.Enabled).ShouldBe(false); + result.DefaultAuthorizationStrategiesForCRUD[0].AuthorizationStrategies.ToList().First().AuthStrategyName.ShouldBe(testAuthStrategy.DisplayName); + } [Test] public void ShouldGetDefaultAuthorizationStrategiesForParentResourcesWithChildrenByClaimSetId() { - var testApplication = new Application - { - ApplicationName = "TestApplicationName" - }; - Save(testApplication); - var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", - Application = testApplication }; Save(testClaimSet); - var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies(testApplication).ToList(); + var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies().ToList(); - var testResourceClaims = SetupParentResourceClaimsWithChildren(new List { testClaimSet }, testApplication); + var testResourceClaims = SetupParentResourceClaimsWithChildren(new List { testClaimSet }); var testAuthStrategies = SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, testResourceClaims.ToList()); - var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId).ToArray(); var testParentResourceClaimsForId = @@ -202,7 +168,8 @@ public void ShouldGetDefaultAuthorizationStrategiesForParentResourcesWithChildre var testAuthStrategiesForParents = testAuthStrategies.Where(x => x.ResourceClaim.ParentResourceClaim == null); - results.Select(x => x.DefaultAuthStrategiesForCRUD[0].AuthorizationStrategies[0].AuthStrategyName).ShouldBe(testAuthStrategiesForParents.Select(x => x.AuthorizationStrategy.AuthorizationStrategyName), true); + results.Select(x => x.DefaultAuthorizationStrategiesForCRUD[0].AuthorizationStrategies.ToList().First().AuthStrategyName).ShouldBe(testAuthStrategiesForParents.Select(x => + x.AuthorizationStrategies.Single().AuthorizationStrategy.AuthorizationStrategyName), true); foreach (var testParentResourceClaim in testParentResourceClaimsForId) { @@ -210,12 +177,12 @@ public void ShouldGetDefaultAuthorizationStrategiesForParentResourcesWithChildre var testAuthStrategiesForChildren = testAuthStrategies.Where(x => x.ResourceClaim.ParentResourceClaimId == testParentResourceClaim.ResourceClaimId); - parentResult.Children.Select(x => x.DefaultAuthStrategiesForCRUD[0].AuthorizationStrategies[0].AuthStrategyName).ShouldBe(testAuthStrategiesForChildren.Select(x => x.AuthorizationStrategy.AuthorizationStrategyName), true); + parentResult.Children.Select(x => x.DefaultAuthorizationStrategiesForCRUD[0].AuthorizationStrategies.ToList().First().AuthStrategyName).ShouldBe(testAuthStrategiesForChildren.Select(x => x.AuthorizationStrategies.Single() + .AuthorizationStrategy.AuthorizationStrategyName), true); } - } - private IReadOnlyCollection SetupApplicationWithClaimSets(Application testApplication, int claimSetCount = 5) + private IReadOnlyCollection SetupApplicationWithClaimSets(int claimSetCount = 5) { var testClaimSetNames = Enumerable.Range(1, claimSetCount) .Select((x, index) => $"TestClaimSetName{index:N}") @@ -225,7 +192,6 @@ private IReadOnlyCollection SetupApplicationWithClaimSets(Application .Select(x => new ClaimSet { ClaimSetName = x, - Application = testApplication }) .ToArray(); @@ -234,26 +200,25 @@ private IReadOnlyCollection SetupApplicationWithClaimSets(Application return testClaimSets; } - private IReadOnlyCollection SetupParentResourceClaims(IEnumerable testClaimSets, Application testApplication, int resourceClaimCount = 5) + private IReadOnlyCollection SetupParentResourceClaims(IEnumerable testClaimSets, IList resouceClaimsIds) { - var claimSetResourceClaims = new List(); + var claimSetResourceClaims = new List(); foreach (var claimSet in testClaimSets) { - foreach (var index in Enumerable.Range(1, resourceClaimCount)) + foreach (var index in resouceClaimsIds) { + var rcName = $"{index}{claimSet.ClaimSetName}"; var resourceClaim = new ResourceClaim { - ClaimName = $"TestResourceClaim{index:N}", - DisplayName = $"TestResourceClaim{index:N}", - ResourceName = $"TestResourceClaim{index:N}", - Application = testApplication + ClaimName = rcName, + ResourceName = rcName, }; var action = new Action { ActionName = ActionName.Create.Value, ActionUri = "create" }; - var claimSetResourceClaim = new ClaimSetResourceClaim + var claimSetResourceClaim = new ClaimSetResourceClaimAction { ResourceClaim = resourceClaim, Action = action, ClaimSet = claimSet }; @@ -266,7 +231,7 @@ private IReadOnlyCollection SetupParentResourceClaims(IEn return claimSetResourceClaims; } - private IReadOnlyCollection SetupParentResourceClaimsWithChildren(IEnumerable testClaimSets, Application testApplication, int resourceClaimCount = 5, int childResourceClaimCount = 3) + private IReadOnlyCollection SetupParentResourceClaimsWithChildren(IEnumerable testClaimSets, int resourceClaimCount = 5, int childResourceClaimCount = 1) { var parentResourceClaims = new List(); var childResourceClaims = new List(); @@ -275,19 +240,15 @@ private IReadOnlyCollection SetupParentResourceClaimsWith var resourceClaim = new ResourceClaim { ClaimName = $"TestParentResourceClaim{parentIndex:N}", - DisplayName = $"TestParentResourceClaim{parentIndex:N}", ResourceName = $"TestParentResourceClaim{parentIndex:N}", - Application = testApplication }; parentResourceClaims.Add(resourceClaim); childResourceClaims.AddRange(Enumerable.Range(1, childResourceClaimCount) .Select(childIndex => new ResourceClaim { - ClaimName = $"TestChildResourceClaim{childIndex:N}", - DisplayName = $"TestChildResourceClaim{childIndex:N}", - ResourceName = $"TestChildResourceClaim{childIndex:N}", - Application = testApplication, + ClaimName = $"TestChildResourceClaim{resourceClaim.ClaimName}", + ResourceName = $"TestChildResourceClaim{resourceClaim.ClaimName}", ParentResourceClaim = resourceClaim, ParentResourceClaimId = resourceClaim.ResourceClaimId })); @@ -296,7 +257,7 @@ private IReadOnlyCollection SetupParentResourceClaimsWith Save(parentResourceClaims.Cast().ToArray()); Save(childResourceClaims.Cast().ToArray()); - var claimSetResourceClaims = new List(); + var claimSetResourceClaims = new List(); var claimSets = testClaimSets.ToList(); foreach (var claimSet in claimSets) { @@ -307,7 +268,7 @@ private IReadOnlyCollection SetupParentResourceClaimsWith ActionName = ActionName.Create.Value, ActionUri = "create" }; - var claimSetResourceClaim = new ClaimSetResourceClaim + var claimSetResourceClaim = new ClaimSetResourceClaimAction { ResourceClaim = childResourceClaims[index - 1], Action = action, @@ -319,7 +280,7 @@ private IReadOnlyCollection SetupParentResourceClaimsWith Save(claimSetResourceClaims.Cast().ToArray()); - claimSetResourceClaims = new List(); + claimSetResourceClaims = new List(); foreach (var claimSet in claimSets) { foreach (var index in Enumerable.Range(1, resourceClaimCount)) @@ -330,7 +291,7 @@ private IReadOnlyCollection SetupParentResourceClaimsWith ActionName = ActionName.Create.Value, ActionUri = "create" }; - var claimSetResourceClaim = new ClaimSetResourceClaim + var claimSetResourceClaim = new ClaimSetResourceClaimAction { ResourceClaim = parentResource, Action = action, @@ -339,7 +300,7 @@ private IReadOnlyCollection SetupParentResourceClaimsWith claimSetResourceClaims.Add(claimSetResourceClaim); var childResources = childResourceClaims .Where(x => x.ParentResourceClaimId == parentResource.ResourceClaimId).Select(x => - new ClaimSetResourceClaim + new ClaimSetResourceClaimAction { ResourceClaim = x, Action = action, diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/OverrideDefaultAuthorizationStrategyCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/OverrideDefaultAuthorizationStrategyCommandTests.cs new file mode 100644 index 000000000..059e37fa1 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/OverrideDefaultAuthorizationStrategyCommandTests.cs @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using FluentValidation; +using NUnit.Framework; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; + +namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; + +[TestFixture] +public class OverrideDefaultAuthorizationStrategyCommandTests : SecurityDataTestBase +{ + [Test] + public void ShouldOverrideAuthorizationStrategiesForParentResourcesOnClaimSet() + { + InitializeData(out var testClaimSet, out var appAuthorizationStrategies, out var testResource1ToEdit, out var testResource2ToNotEdit); + var overrides = new List(); + if (appAuthorizationStrategies != null) + { + foreach (var appAuthorizationStrategy in appAuthorizationStrategies) + { + overrides.Add(new AuthorizationStrategy + { + AuthStrategyId = appAuthorizationStrategy.AuthorizationStrategyId, + AuthStrategyName = appAuthorizationStrategy.AuthorizationStrategyName + }); + } + } + + var overrideModel = new OverrideAuthorizationStrategyModel + { + ResourceClaimId = testResource1ToEdit.ResourceClaimId, + ClaimSetId = testClaimSet.ClaimSetId, + ClaimSetResourceClaimActionAuthStrategyOverrides = new List { + new ClaimSetResourceClaimActionAuthStrategies + { + ActionId = 1, + ActionName= "Create", + AuthorizationStrategies = overrides + } + } + }; + + List resourceClaimsForClaimSet = null; + + using var securityContext = TestContext; + var command = new OverrideDefaultAuthorizationStrategyCommand(securityContext); + command.Execute(overrideModel); + var getResourcesByClaimSetIdQuery = new GetResourcesByClaimSetIdQuery(securityContext, SecurityDataTestBase.Mapper()); + resourceClaimsForClaimSet = getResourcesByClaimSetIdQuery.AllResources(testClaimSet.ClaimSetId).ToList(); + + var resultResourceClaim1 = + resourceClaimsForClaimSet.Single(x => x.Id == overrideModel.ResourceClaimId); + + resultResourceClaim1.AuthorizationStrategyOverridesForCRUD.Count.ShouldBe(1); + resultResourceClaim1.AuthorizationStrategyOverridesForCRUD[0].ActionName.ShouldBe("Create"); + resultResourceClaim1.AuthorizationStrategyOverridesForCRUD[0].AuthorizationStrategies.First().AuthStrategyName.ShouldBe("TestAuthStrategy1"); + + var resultResourceClaim2 = + resourceClaimsForClaimSet.Single(x => x.Id == testResource2ToNotEdit.ResourceClaimId); + + resultResourceClaim2.AuthorizationStrategyOverridesForCRUD.ShouldBeEmpty(); + } + + [Test] + public void ShouldOverrideAuthorizationStrategiesForSpecificResourcesOnClaimSet() + { + InitializeData(out var testClaimSet, out var appAuthorizationStrategies, out var testResource1ToEdit, out var testResource2ToNotEdit); + + var overrides = new List(); + if (appAuthorizationStrategies != null) + { + foreach (var appAuthorizationStrategy in appAuthorizationStrategies) + { + overrides.Add(appAuthorizationStrategy.AuthorizationStrategyId); + } + } + var overrideModel = new OverrideAuthStrategyOnClaimSetModel + { + ResourceClaimId = testResource1ToEdit.ResourceClaimId, + ClaimSetId = testClaimSet.ClaimSetId, + ActionName = "Create", + AuthStrategyIds = overrides + }; + + List resourceClaimsForClaimSet = null; + + using var securityContext = TestContext; + var command = new OverrideDefaultAuthorizationStrategyCommand(securityContext); + command.ExecuteOnSpecificAction(overrideModel); + var getResourcesByClaimSetIdQuery = new GetResourcesByClaimSetIdQuery(securityContext, SecurityDataTestBase.Mapper()); + resourceClaimsForClaimSet = getResourcesByClaimSetIdQuery.AllResources(testClaimSet.ClaimSetId).ToList(); + + var resultResourceClaim1 = + resourceClaimsForClaimSet.Single(x => x.Id == overrideModel.ResourceClaimId); + + resultResourceClaim1.AuthorizationStrategyOverridesForCRUD.Count.ShouldBe(1); + resultResourceClaim1.AuthorizationStrategyOverridesForCRUD[0].ActionName.ShouldBe("Create"); + resultResourceClaim1.AuthorizationStrategyOverridesForCRUD[0].AuthorizationStrategies.Count().ShouldBe(4); + + var resultResourceClaim2 = + resourceClaimsForClaimSet.Single(x => x.Id == testResource2ToNotEdit.ResourceClaimId); + + resultResourceClaim2.AuthorizationStrategyOverridesForCRUD.ShouldBeEmpty(); + } + + [Test] + public void ShouldOverrideAuthorizationStrategiesForSpecificResourcesOnClaimSetDefaultAuth() + { + InitializeData(out var testClaimSet, out var appAuthorizationStrategies, out var testResource1ToEdit, out var testResource2ToNotEdit); + + var overrides = appAuthorizationStrategies.Select(a => a.AuthorizationStrategyId).ToList(); + + var resourceClaimActionParent = TestContext.ResourceClaimActions.First(rca => rca.ResourceClaimId == testResource1ToEdit.ResourceClaimId && rca.Action.ActionName == "Create"); + + var resourceClaimActionAuthStrategiesParent = TestContext.ResourceClaimActionAuthorizationStrategies.First(rcaa => rcaa.ResourceClaimActionId == resourceClaimActionParent.ResourceClaimActionId); + + overrides.Add(resourceClaimActionAuthStrategiesParent.AuthorizationStrategyId); + + var overrideModel = new OverrideAuthStrategyOnClaimSetModel + { + ResourceClaimId = testResource1ToEdit.ResourceClaimId, + ClaimSetId = testClaimSet.ClaimSetId, + ActionName = "Create", + AuthStrategyIds = overrides + }; + + List resourceClaimsForClaimSet = null; + + using var securityContext = TestContext; + var command = new OverrideDefaultAuthorizationStrategyCommand(securityContext); + command.ExecuteOnSpecificAction(overrideModel); + var getResourcesByClaimSetIdQuery = new GetResourcesByClaimSetIdQuery(securityContext, SecurityDataTestBase.Mapper()); + resourceClaimsForClaimSet = getResourcesByClaimSetIdQuery.AllResources(testClaimSet.ClaimSetId).ToList(); + + var resultResourceClaim1 = + resourceClaimsForClaimSet.Single(x => x.Id == overrideModel.ResourceClaimId); + + resultResourceClaim1.AuthorizationStrategyOverridesForCRUD.Count.ShouldBe(1); + resultResourceClaim1.AuthorizationStrategyOverridesForCRUD[0].ActionName.ShouldBe("Create"); + + var resourceClaimActionAuthorizationStrategies = new GetResourceClaimActionAuthorizationStrategiesQuery(securityContext, null).Execute(new Common.Infrastructure.CommonQueryParams(), null); + var authStrategyName = resourceClaimActionAuthorizationStrategies + .FirstOrDefault(p => p.ResourceClaimId == overrideModel.ResourceClaimId).AuthorizationStrategiesForActions + .FirstOrDefault(s => s.ActionName.Equals(overrideModel.ActionName, StringComparison.CurrentCultureIgnoreCase)).AuthorizationStrategies.FirstOrDefault().AuthStrategyName; + + resultResourceClaim1.AuthorizationStrategyOverridesForCRUD[0].AuthorizationStrategies.Any(x => x.AuthStrategyName.Equals(authStrategyName)).ShouldBeFalse(); + + var resultResourceClaim2 = + resourceClaimsForClaimSet.Single(x => x.Id == testResource2ToNotEdit.ResourceClaimId); + + resultResourceClaim2.AuthorizationStrategyOverridesForCRUD.ShouldBeEmpty(); + } + + [Test] + public void ShouldThrowErrorWhenActionIsNotEnabledForAResource() + { + InitializeData(out var testClaimSet, out var appAuthorizationStrategies, out var testResource1ToEdit, out var testResource2ToNotEdit); + var action = "Update"; + var overrides = new List(); + if (appAuthorizationStrategies != null) + { + foreach (var appAuthorizationStrategy in appAuthorizationStrategies) + { + overrides.Add(appAuthorizationStrategy.AuthorizationStrategyId); + } + } + var overrideModel = new OverrideAuthStrategyOnClaimSetModel + { + ResourceClaimId = testResource1ToEdit.ResourceClaimId, + ClaimSetId = testClaimSet.ClaimSetId, + ActionName = action, + AuthStrategyIds = overrides + }; + + var badRequestException = Assert.Throws(() => + { + using var securityContext = TestContext; + var command = new OverrideDefaultAuthorizationStrategyCommand(securityContext); + command.ExecuteOnSpecificAction(overrideModel); + }); + badRequestException.ShouldNotBeNull(); + badRequestException.Errors.First().ErrorMessage.ShouldContain($"{action} action is not enabled for the resource claim with"); + + } + + private void InitializeData(out ClaimSet testClaimSet, out List appAuthorizationStrategies, out Security.DataAccess.Models.ResourceClaim testResource1ToEdit, out Security.DataAccess.Models.ResourceClaim testResource2ToNotEdit) + { + testClaimSet = new ClaimSet + { + ClaimSetName = "TestClaimSet", + }; + Save(testClaimSet); + + appAuthorizationStrategies = SetupApplicationAuthorizationStrategies().ToList(); + var parentRcNames = UniqueNameList("ParentRc", 2); + + var testResourceClaims = SetupClaimSetResourceClaimActions( + testClaimSet, parentRcNames, UniqueNameList("Child", 1)); + + SetupResourcesWithDefaultAuthorizationStrategies( + appAuthorizationStrategies, testResourceClaims.ToList()); + + testResource1ToEdit = testResourceClaims.Select(x => x.ResourceClaim) + .Single(x => x.ResourceName == parentRcNames.First()); + testResource2ToNotEdit = testResourceClaims.Select(x => x.ResourceClaim) + .Single(x => x.ResourceName == parentRcNames.Last()); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/OverrideDefaultAuthorizationStrategyV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/OverrideDefaultAuthorizationStrategyV53ServiceTests.cs deleted file mode 100644 index 4950dd8c5..000000000 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/OverrideDefaultAuthorizationStrategyV53ServiceTests.cs +++ /dev/null @@ -1,148 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using System.Linq; -using NUnit.Framework; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using Shouldly; -using AutoMapper; -using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application; -using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet; -using EdFi.Ods.AdminApi.Infrastructure; - -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; - -[TestFixture] -public class OverrideDefaultAuthorizationStrategyV53ServiceTests : SecurityData53TestBase -{ - [SetUp] - public void Init() - { - var config = new MapperConfiguration(cfg => cfg.AddProfile()); - } - - [Test] - public void ShouldOverrideAuthorizationStrategiesForParentResourcesOnClaimSet() - { - // Arrange - var testApplication = new Application - { - ApplicationName = "TestApplicationName" - }; - Save(testApplication); - - var testClaimSet = new ClaimSet - { - ClaimSetName = "TestClaimSet", - Application = testApplication - }; - Save(testClaimSet); - - var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies(testApplication).ToList(); - var testResourceClaims = SetupParentResourceClaimsWithChildren(testClaimSet, testApplication); - SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, testResourceClaims.ToList()); - - var testResource1ToEdit = testResourceClaims.Select(x => x.ResourceClaim).Single(x => x.ResourceName == "TestParentResourceClaim1"); - var testResource2ToNotEdit = testResourceClaims.Select(x => x.ResourceClaim).Single(x => x.ResourceName == "TestParentResourceClaim2"); - - var overrideModel = new OverrideAuthorizationStrategyModel - { - ResourceClaimId = testResource1ToEdit.ResourceClaimId, - ClaimSetId = testClaimSet.ClaimSetId, - AuthorizationStrategyForCreate = new int[1] { appAuthorizationStrategies.Single(x => x.AuthorizationStrategyName == "TestAuthStrategy4").AuthorizationStrategyId }, - AuthorizationStrategyForRead = new int[0], - AuthorizationStrategyForUpdate = new int[0], - AuthorizationStrategyForDelete = new int[0], - }; - - // Act - using var securityContext = TestContext; - var command = new OverrideDefaultAuthorizationStrategyV53Service(securityContext); - command.Execute(overrideModel); - - // Assert - var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId).ToList(); - - var resultResourceClaim1 = resourceClaimsForClaimSet.Single(x => x.Id == overrideModel.ResourceClaimId); - - resultResourceClaim1.AuthStrategyOverridesForCRUD[0].AuthorizationStrategies[0].AuthStrategyName.ShouldBe("TestAuthStrategy4"); - resultResourceClaim1.AuthStrategyOverridesForCRUD[1].ShouldBeNull(); - resultResourceClaim1.AuthStrategyOverridesForCRUD[2].ShouldBeNull(); - resultResourceClaim1.AuthStrategyOverridesForCRUD[3].ShouldBeNull(); - - var resultResourceClaim2 = resourceClaimsForClaimSet.Single(x => x.Id == testResource2ToNotEdit.ResourceClaimId); - - resultResourceClaim2.AuthStrategyOverridesForCRUD[0].ShouldBeNull(); - resultResourceClaim2.AuthStrategyOverridesForCRUD[1].ShouldBeNull(); - resultResourceClaim2.AuthStrategyOverridesForCRUD[2].ShouldBeNull(); - resultResourceClaim2.AuthStrategyOverridesForCRUD[3].ShouldBeNull(); - } - - [Test] - public void ShouldOverrideAuthorizationStrategiesForChildResourcesOnClaimSet() - { - // Arrange - var testApplication = new Application - { - ApplicationName = "TestApplicationName" - }; - Save(testApplication); - - var testClaimSet = new ClaimSet - { - ClaimSetName = "TestClaimSet", - Application = testApplication - }; - Save(testClaimSet); - - var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies(testApplication).ToList(); - var testResourceClaims = SetupParentResourceClaimsWithChildren(testClaimSet, testApplication); - - SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, testResourceClaims.ToList()); - - var testParentResource = testResourceClaims.Select(x => x.ResourceClaim).Single(x => x.ResourceName == "TestParentResourceClaim1"); - var testChildResourceToEdit = testResourceClaims.Select(x => x.ResourceClaim).Single(x => - x.ResourceName == "TestChildResourceClaim1" && - x.ParentResourceClaimId == testParentResource.ResourceClaimId); - var testChildResourceNotToEdit = testResourceClaims.Select(x => x.ResourceClaim).Single(x => - x.ResourceName == "TestChildResourceClaim2" && - x.ParentResourceClaimId == testParentResource.ResourceClaimId); - - var overrideModel = new OverrideAuthorizationStrategyModel - { - ResourceClaimId = testChildResourceToEdit.ResourceClaimId, - ClaimSetId = testClaimSet.ClaimSetId, - AuthorizationStrategyForCreate = new int[1] { appAuthorizationStrategies.Single(x => x.AuthorizationStrategyName == "TestAuthStrategy4").AuthorizationStrategyId }, - AuthorizationStrategyForRead = new int[0], - AuthorizationStrategyForUpdate = new int[0], - AuthorizationStrategyForDelete = new int[0] - }; - - // Act - using var securityContext = TestContext; - var command = new OverrideDefaultAuthorizationStrategyV53Service(securityContext); - command.Execute(overrideModel); - - // Assert - var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId).ToList(); - - var resultParentResource = resourceClaimsForClaimSet.Single(x => x.Id == testParentResource.ResourceClaimId); - var resultChildResource1 = - resultParentResource.Children.Single(x => x.Id == testChildResourceToEdit.ResourceClaimId); - - resultChildResource1.AuthStrategyOverridesForCRUD[0].AuthorizationStrategies[0].AuthStrategyName.ShouldBe("TestAuthStrategy4"); - resultChildResource1.AuthStrategyOverridesForCRUD[1].ShouldBeNull(); - resultChildResource1.AuthStrategyOverridesForCRUD[2].ShouldBeNull(); - resultChildResource1.AuthStrategyOverridesForCRUD[3].ShouldBeNull(); - - var resultResourceClaim2 = resultParentResource.Children.Single(x => x.Id == testChildResourceNotToEdit.ResourceClaimId); - - resultResourceClaim2.AuthStrategyOverridesForCRUD[0].ShouldBeNull(); - resultResourceClaim2.AuthStrategyOverridesForCRUD[1].ShouldBeNull(); - resultResourceClaim2.AuthStrategyOverridesForCRUD[2].ShouldBeNull(); - resultResourceClaim2.AuthStrategyOverridesForCRUD[3].ShouldBeNull(); - } -} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/UpdateResourcesOnClaimSetCommandV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/UpdateResourcesOnClaimSetCommandTests.cs similarity index 57% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/UpdateResourcesOnClaimSetCommandV53ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/UpdateResourcesOnClaimSetCommandTests.cs index 37fd0ac48..c239b3273 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/UpdateResourcesOnClaimSetCommandV53ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/UpdateResourcesOnClaimSetCommandTests.cs @@ -2,43 +2,38 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; -using System; -using System.Linq; -using NUnit.Framework; using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using Moq; +using NUnit.Framework; using Shouldly; using System.Collections.Generic; -using Moq; - -using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application; -using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet; +using System.Linq; +using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; [TestFixture] -public class UpdateResourcesOnClaimSetCommandV53ServiceTests : SecurityData53TestBase +public class UpdateResourcesOnClaimSetCommandTests : SecurityDataTestBase { [Test] public void ShouldUpdateResourcesOnClaimSet() { - var testApplication = new Application - { - ApplicationName = $"Test Application {DateTime.Now:O}" - }; - Save(testApplication); - - var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet", Application = testApplication }; + var testClaimSet = new ClaimSet { ClaimSetName = "TestClaimSet" }; Save(testClaimSet); - var testResources = SetupParentResourceClaimsWithChildren(testClaimSet, testApplication, 2, 1); + var parentRcNames = UniqueNameList("ParentRc", 2); + var childName = "ChildRc098"; + var testResources = SetupClaimSetResourceClaimActions(testClaimSet, parentRcNames, + new List { childName }); - var testParentResource = testResources.Single(x => x.ResourceClaim.ResourceName == "TestParentResourceClaim1"); - var secondTestParentResource = testResources.Single(x => x.ResourceClaim.ResourceName == "TestParentResourceClaim2"); + var testParentResource = testResources.Single(x => x.ResourceClaim.ResourceName == parentRcNames.First()); + var secondTestParentResource = testResources.Single(x => x.ResourceClaim.ResourceName == parentRcNames.Last()); + var firstParentChildName = $"{childName}-{parentRcNames.First()}"; using var securityContext = TestContext; - var testChildResource1ToEdit = securityContext.ResourceClaims.Single(x => x.ResourceName == "TestChildResourceClaim1" && x.ParentResourceClaimId == testParentResource.ResourceClaim.ResourceClaimId); + var testChildResource1ToEdit = securityContext.ResourceClaims.Single(x => x.ResourceName == firstParentChildName && x.ParentResourceClaimId == testParentResource.ResourceClaim.ResourceClaimId); var addedResourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); addedResourceClaimsForClaimSet.Count.ShouldBe(2); @@ -49,18 +44,24 @@ public void ShouldUpdateResourcesOnClaimSet() { Id = testParentResource.ResourceClaim.ResourceClaimId, Name = testParentResource.ResourceClaim.ResourceName, - Create = false, - Read = false, - Update = true, - Delete = true, + Actions = new List + { + new ResourceClaimAction{ Name = "Create", Enabled = false }, + new ResourceClaimAction{ Name = "Read", Enabled = false }, + new ResourceClaimAction{ Name = "Update", Enabled = true }, + new ResourceClaimAction{ Name = "Delete", Enabled = true} + }, Children = new List {new ResourceClaim { Id = testChildResource1ToEdit.ResourceClaimId, Name = testChildResource1ToEdit.ResourceName, - Create = false, - Read = false, - Update = true, - Delete = true + Actions = new List + { + new ResourceClaimAction{ Name = "Create", Enabled = false }, + new ResourceClaimAction{ Name = "Read", Enabled = false }, + new ResourceClaimAction{ Name = "Update", Enabled = true }, + new ResourceClaimAction{ Name = "Delete", Enabled = true} + } } } }; @@ -73,16 +74,13 @@ public void ShouldUpdateResourcesOnClaimSet() updateResourcesOnClaimSetModel.Setup(x => x.ClaimSetId).Returns(testClaimSet.ClaimSetId); updateResourcesOnClaimSetModel.Setup(x => x.ResourceClaims).Returns(updatedResourceClaims); - using var securityContext53 = CreateDbContext(); + using var context = CreateDbContext(); var addOrEditResourcesOnClaimSetCommand = new AddOrEditResourcesOnClaimSetCommand( - new EditResourceOnClaimSetCommand(new StubOdsSecurityModelVersionResolver.V3_5(), - new EditResourceOnClaimSetCommandV53Service(securityContext53), null), - new GetResourceClaims53Query(securityContext53), - new OverrideDefaultAuthorizationStrategyCommand( - new StubOdsSecurityModelVersionResolver.V3_5(), - new OverrideDefaultAuthorizationStrategyV53Service(securityContext53), null)); + new EditResourceOnClaimSetCommand(context), + new GetResourceClaimsQuery(context, Testing.GetAppSettings()), + new OverrideDefaultAuthorizationStrategyCommand(context)); - var command = new UpdateResourcesOnClaimSetCommandV53Service(securityContext53, addOrEditResourcesOnClaimSetCommand); + var command = new UpdateResourcesOnClaimSetCommand(context, addOrEditResourcesOnClaimSetCommand); command.Execute(updateResourcesOnClaimSetModel.Object); var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddApiClientCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddApiClientCommandTests.cs new file mode 100644 index 000000000..ff850f589 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddApiClientCommandTests.cs @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Microsoft.Extensions.Options; +using NUnit.Framework; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +internal class AddApiClientCommandTests : PlatformUsersContextTestBase +{ + private IOptions _options { get; set; } + private int applicationId { get; set; } + + [SetUp] + public new virtual async Task SetUp() + { + AppSettings appSettings = new() + { + PreventDuplicateApplications = false + }; + _options = Options.Create(appSettings); + await Task.Yield(); + + var vendor = new Vendor + { + VendorId = 0, + VendorNamespacePrefixes = [new() { NamespacePrefix = "http://tests.com" }], + VendorName = "Integration Tests" + }; + + var application = new Application + { + ApplicationName = "Test Application", + ClaimSetName = "FakeClaimSet", + OperationalContextUri = "http://test.com", + Profiles = null, + Vendor = vendor + }; + + Save(application); + + applicationId = application.ApplicationId; + } + + [Test] + public void ShouldFailForInvalidApplication() + { + Transaction(usersContext => + { + var command = new AddApiClientCommand(usersContext); + var newApiClient = new TestApiClient + { + Name = "Test ApiClient", + ApplicationId = 0, + IsApproved = true, + OdsInstanceIds = [1, 2] + }; + + Assert.Throws(() => command.Execute(newApiClient, _options)); + }); + } + + [Test] + public void ShouldCreateApiClientWithOdsInstances() + { + var vendor = new Vendor + { + VendorId = 0, + VendorNamespacePrefixes = [new() { NamespacePrefix = "http://tests.com" }], + VendorName = "Integration Tests" + }; + + var application = new Application + { + ApplicationName = "Test Application", + ClaimSetName = "FakeClaimSet", + OperationalContextUri = "http://test.com", + Profiles = null, + Vendor = vendor + }; + + Save(application); + + Transaction(usersContext => + { + var command = new AddApiClientCommand(usersContext); + var newApiClient = new TestApiClient + { + Name = "Test ApiClient", + ApplicationId = application.ApplicationId, + IsApproved = true, + OdsInstanceIds = [1, 2] + }; + + command.Execute(newApiClient, _options); + }); + } + + [Test] + public void ShouldCreateApiClientWithOutOdsInstances() + { + var vendor = new Vendor + { + VendorId = 0, + VendorNamespacePrefixes = [new() { NamespacePrefix = "http://tests.com" }], + VendorName = "Integration Tests" + }; + + var application = new Application + { + ApplicationName = "Test Application", + ClaimSetName = "FakeClaimSet", + OperationalContextUri = "http://test.com", + Profiles = null, + Vendor = vendor + }; + + Save(application); + + Transaction(usersContext => + { + var command = new AddApiClientCommand(usersContext); + var newApiClient = new TestApiClient + { + Name = "Test ApiClient", + ApplicationId = application.ApplicationId, + IsApproved = true, + OdsInstanceIds = null // No OdsInstanceIds provided + }; + + command.Execute(newApiClient, _options); + }); + } + + private class TestApiClient : IAddApiClientModel + { + public string Name { get; set; } + public bool IsApproved { get; set; } + public int ApplicationId { get; set; } + public IEnumerable OdsInstanceIds { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddApiClientOdsInstanceCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddApiClientOdsInstanceCommandTests.cs new file mode 100644 index 000000000..19cf4dda6 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddApiClientOdsInstanceCommandTests.cs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Moq; +using NUnit.Framework; +using Shouldly; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class AddApiClientOdsInstanceCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldAddApiClient() + { + var odsInstance = new OdsInstance + { + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + var application = new Application + { + ApplicationName = "test application", + ClaimSetName = "test claim set", + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + }; + var apiClient = new ApiClient + { + Application = application, + Key = "key", + Secret = "secret", + Name = application.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active" + }; + + var apiClientOdsInstance = new ApiClientOdsInstance + { + ApiClient = apiClient, + OdsInstance = odsInstance, + }; + Save(odsInstance, application, apiClient, apiClientOdsInstance); + apiClientOdsInstance.ShouldNotBeNull(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddApplicationCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddApplicationCommandTests.cs index f599fe3b5..8eb214cda 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddApplicationCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddApplicationCommandTests.cs @@ -6,10 +6,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using EdFi.Admin.DataAccess.Models; -using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Settings; using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; using NUnit.Framework; using Shouldly; @@ -18,6 +21,17 @@ namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; [TestFixture] public class AddApplicationCommandTests : PlatformUsersContextTestBase { + private IOptions _options { get; set; } + + [OneTimeSetUp] + public virtual async Task FixtureSetup() + { + AppSettings appSettings = new AppSettings(); + appSettings.PreventDuplicateApplications = false; + _options = Options.Create(appSettings); + await Task.Yield(); + } + [Test] public void ShouldFailForInvalidVendor() { @@ -31,16 +45,16 @@ public void ShouldFailForInvalidVendor() Transaction(usersContext => { - var command = new AddApplicationCommand(usersContext, new InstanceContext()); + var command = new AddApplicationCommand(usersContext); var newApplication = new TestApplication { ApplicationName = "Production-Test Application", ClaimSetName = "FakeClaimSet", - ProfileId = 0, + ProfileIds = null, VendorId = 0 }; - Assert.Throws(() => command.Execute(newApplication)); + Assert.Throws(() => command.Execute(newApplication, _options)); }); } @@ -55,11 +69,9 @@ public void ProfileShouldBeOptional() var odsInstance = new OdsInstance { - Name = "test ods instance", - InstanceType = "test type", - Status = "test status", - IsExtended = true, - Version = "test version" + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" }; Save(vendor, odsInstance); @@ -68,92 +80,98 @@ public void ProfileShouldBeOptional() Transaction(usersContext => { - var command = new AddApplicationCommand(usersContext, new InstanceContext()); + var command = new AddApplicationCommand(usersContext); var newApplication = new TestApplication { ApplicationName = "Test Application", ClaimSetName = "FakeClaimSet", - ProfileId = null, - OdsInstanceId = odsInstance.OdsInstanceId, + ProfileIds = null, VendorId = vendor.VendorId, - EducationOrganizationIds = new List { 12345, 67890 } + EducationOrganizationIds = new List { 12345, 67890, 5000000005 }, + OdsInstanceIds = new List { odsInstance.OdsInstanceId }, }; - result = command.Execute(newApplication); + result = command.Execute(newApplication, _options); }); Transaction(usersContext => { var persistedApplication = usersContext.Applications - .Include(x => x.Profiles) - .Include(x => x.ApplicationEducationOrganizations) - .Include(x => x.Vendor) - .Include(x => x.ApiClients) - .Include(x => x.OdsInstance) - .Single(a => a.ApplicationId == result.ApplicationId); + .Include(a => a.ApplicationEducationOrganizations) + .Include(a => a.Vendor) + .Include(a => a.ApiClients) + .FirstOrDefault(a => a.ApplicationId == result.ApplicationId); persistedApplication.ClaimSetName.ShouldBe("FakeClaimSet"); persistedApplication.Profiles.Count.ShouldBe(0); - persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(2); - persistedApplication.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890).ShouldBeTrue(); + persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(3); + persistedApplication.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890 || o.EducationOrganizationId == 5000000005).ShouldBeTrue(); persistedApplication.Vendor.VendorId.ShouldBeGreaterThan(0); persistedApplication.Vendor.VendorId.ShouldBe(vendor.VendorId); - persistedApplication.OdsInstance.OdsInstanceId.ShouldBe(odsInstance.OdsInstanceId); - persistedApplication.ApiClients.Count.ShouldBe(1); - var apiClient = persistedApplication.ApiClients.First(); + var apiClient = persistedApplication.ApiClients.FirstOrDefault(); apiClient.Name.ShouldBe("Test Application"); - apiClient.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890).ShouldBeTrue(); + apiClient.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890 || o.EducationOrganizationId == 5000000005).ShouldBeTrue(); apiClient.Key.ShouldBe(result.Key); apiClient.Secret.ShouldBe(result.Secret); }); } [Test] - public void OdsInstanceShouldBeOptional() + public void ShouldExecute() { + const string OdsInstanceName = "Test Instance"; var vendor = new Vendor { VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://tests.com" } }, VendorName = "Integration Tests" }; - Save(vendor); + var profile = new Profile + { + ProfileName = "Test Profile" + }; - AddApplicationResult result = null; + var odsInstance = new OdsInstance + { + Name = OdsInstanceName, + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + Save(vendor, profile, odsInstance); + + AddApplicationResult result = null; Transaction(usersContext => { - var command = new AddApplicationCommand(usersContext, new InstanceContext()); + var command = new AddApplicationCommand(usersContext); var newApplication = new TestApplication { ApplicationName = "Test Application", ClaimSetName = "FakeClaimSet", - ProfileId = null, - OdsInstanceId = null, + ProfileIds = new List() { profile.ProfileId }, VendorId = vendor.VendorId, - EducationOrganizationIds = new List { 12345, 67890 } + EducationOrganizationIds = new List { 12345, 67890, 5000000005 }, + OdsInstanceIds = new List { odsInstance.OdsInstanceId }, }; - result = command.Execute(newApplication); + result = command.Execute(newApplication, _options); }); Transaction(usersContext => { var persistedApplication = usersContext.Applications - .Include(x => x.Profiles) - .Include(x => x.OdsInstance) - .Include(x => x.ApplicationEducationOrganizations) - .Include(x => x.Vendor) - .Include(x => x.ApiClients) - .Single(a => a.ApplicationId == result.ApplicationId); - + .Include(a => a.ApplicationEducationOrganizations) + .Include(a => a.Vendor) + .Include(a => a.Profiles) + .Include(a => a.ApiClients).Single(a => a.ApplicationId == result.ApplicationId); persistedApplication.ClaimSetName.ShouldBe("FakeClaimSet"); - persistedApplication.OdsInstance.ShouldBeNull(); - persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(2); - persistedApplication.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890).ShouldBeTrue(); + persistedApplication.Profiles.Count.ShouldBe(1); + persistedApplication.Profiles.First().ProfileName.ShouldBe("Test Profile"); + persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(3); + persistedApplication.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890 || o.EducationOrganizationId == 5000000005).ShouldBeTrue(); persistedApplication.Vendor.VendorId.ShouldBeGreaterThan(0); persistedApplication.Vendor.VendorId.ShouldBe(vendor.VendorId); @@ -161,22 +179,43 @@ public void OdsInstanceShouldBeOptional() persistedApplication.ApiClients.Count.ShouldBe(1); var apiClient = persistedApplication.ApiClients.First(); apiClient.Name.ShouldBe("Test Application"); - apiClient.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890).ShouldBeTrue(); + apiClient.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890 || o.EducationOrganizationId == 5000000005).ShouldBeTrue(); apiClient.Key.ShouldBe(result.Key); apiClient.Secret.ShouldBe(result.Secret); + + var persistedApiOdsInstances = usersContext.ApiClientOdsInstances.Where(a => a.ApiClient.ApiClientId == apiClient.ApiClientId).ToList(); + + persistedApiOdsInstances.ShouldNotBeNull(); + persistedApiOdsInstances.First().ApiClient.ApiClientId.ShouldBe(apiClient.ApiClientId); + }); + + Transaction(usersContext => + { + var persistedApplication = usersContext.Applications + .Include(p => p.ApiClients) + .Single(a => a.ApplicationId == result.ApplicationId); + var apiClient = persistedApplication.ApiClients.First(); + var apiClientOdsInstance = usersContext.ApiClientOdsInstances + .Include(ac => ac.OdsInstance) + .Include(ac => ac.ApiClient) + .FirstOrDefault(o => o.OdsInstance.OdsInstanceId == odsInstance.OdsInstanceId && o.ApiClient.ApiClientId == apiClient.ApiClientId); + apiClientOdsInstance.ApiClient.ApiClientId.ShouldBe(apiClient.ApiClientId); + apiClientOdsInstance.OdsInstance.OdsInstanceId.ShouldBe(odsInstance.OdsInstanceId); }); } [Test] - public void ShouldExecute() + public void ShouldFailToAddDuplicatedApplication() { - const string odsInstanceName = "Test Instance"; + AppSettings appSettings = new AppSettings(); + appSettings.PreventDuplicateApplications = true; + IOptions options = Options.Create(appSettings); + const string OdsInstanceName = "Test Instance"; var vendor = new Vendor { VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://tests.com" } }, VendorName = "Integration Tests" }; - var profile = new Profile { ProfileName = "Test Profile" @@ -184,66 +223,149 @@ public void ShouldExecute() var odsInstance = new OdsInstance { - Name = odsInstanceName, + Name = OdsInstanceName, InstanceType = "Ods", - IsExtended = false, - Status = "OK", - Version = "1.0.0" + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" }; Save(vendor, profile, odsInstance); - var instanceContext = new InstanceContext + AddApplicationResult result = null; + Transaction(usersContext => + { + var command = new AddApplicationCommand(usersContext); + var newApplication = new TestApplication + { + ApplicationName = "Production-Test Application", + ClaimSetName = "FakeClaimSet", + ProfileIds = [], + VendorId = vendor.VendorId, + EducationOrganizationIds = new List { 12345, 67890, 5000000005 }, + OdsInstanceIds = new List { odsInstance.OdsInstanceId } + }; + + result = command.Execute(newApplication, options); + }); + + Transaction(usersContext => + { + var command = new AddApplicationCommand(usersContext); + var newApplication = new TestApplication + { + ApplicationName = "Production-Test Application", + ClaimSetName = "FakeClaimSet", + ProfileIds = [], + VendorId = vendor.VendorId, + EducationOrganizationIds = new List { 12345, 67890, 5000000005 }, + OdsInstanceIds = new List { odsInstance.OdsInstanceId } + }; + + Assert.Throws(() => command.Execute(newApplication, options)); + }); + } + + [Test] + public void ShouldFailToAddDuplicatedApplicationNullFields() + { + AppSettings appSettings = new AppSettings(); + appSettings.PreventDuplicateApplications = true; + IOptions options = Options.Create(appSettings); + var vendor = new Vendor { - Id = 1, - Name = odsInstanceName + VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://tests.com" } }, + VendorName = "Integration Tests" }; + Save(vendor); AddApplicationResult result = null; Transaction(usersContext => { - var command = new AddApplicationCommand(usersContext, instanceContext); + var command = new AddApplicationCommand(usersContext); var newApplication = new TestApplication { - ApplicationName = "Test Application", + ApplicationName = "Production-Test Application", ClaimSetName = "FakeClaimSet", - ProfileId = profile.ProfileId, - OdsInstanceId = odsInstance.OdsInstanceId, - VendorId = vendor.VendorId, - EducationOrganizationIds = new List { 12345, 67890 } + ProfileIds = null, + VendorId = vendor.VendorId }; - result = command.Execute(newApplication); + result = command.Execute(newApplication, options); }); Transaction(usersContext => { - var persistedApplication = usersContext.Applications - .Include(x => x.Profiles) - .Include(x => x.ApplicationEducationOrganizations) - .Include(x => x.Vendor) - .Include(x => x.ApiClients) - .Include(x => x.OdsInstance) - .Single(a => a.ApplicationId == result.ApplicationId); + var command = new AddApplicationCommand(usersContext); + var newApplication = new TestApplication + { + ApplicationName = "Production-Test Application", + ClaimSetName = "FakeClaimSet", + ProfileIds = null, + VendorId = vendor.VendorId + }; - persistedApplication.ClaimSetName.ShouldBe("FakeClaimSet"); - persistedApplication.Profiles.Count.ShouldBe(1); - persistedApplication.Profiles.First().ProfileName.ShouldBe("Test Profile"); - persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(2); - persistedApplication.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890).ShouldBeTrue(); + Assert.Throws(() => command.Execute(newApplication, options)); + }); + } - persistedApplication.Vendor.VendorId.ShouldBeGreaterThan(0); - persistedApplication.Vendor.VendorId.ShouldBe(vendor.VendorId); + [Test] + public void ShouldExecuteWithDuplicatedNamesDifferentVendor() + { + AppSettings appSettings = new AppSettings(); + appSettings.PreventDuplicateApplications = true; + IOptions options = Options.Create(appSettings); + var vendor = new Vendor + { + VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://tests.com" } }, + VendorName = "Integration Tests" + }; - persistedApplication.ApiClients.Count.ShouldBe(1); - var apiClient = persistedApplication.ApiClients.First(); - apiClient.Name.ShouldBe("Test Application"); - apiClient.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890).ShouldBeTrue(); - apiClient.Key.ShouldBe(result.Key); - apiClient.Secret.ShouldBe(result.Secret); + Save(vendor); + + var secondVendor = new Vendor + { + VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://tests.com" } }, + VendorName = "Second Integration Tests" + }; + Save(secondVendor); + AddApplicationResult result = null; + AddApplicationResult secondResult = null; + Transaction(usersContext => + { + var command = new AddApplicationCommand(usersContext); + var newApplication = new TestApplication + { + ApplicationName = "Production-Test Application-Duplicated-Name", + ClaimSetName = "FakeClaimSet", + ProfileIds = null, + VendorId = vendor.VendorId + }; + + result = command.Execute(newApplication, options); + }); - persistedApplication.OdsInstance.ShouldNotBeNull(); - persistedApplication.OdsInstance.Name.ShouldBe(odsInstanceName); + Transaction(usersContext => + { + var command = new AddApplicationCommand(usersContext); + var newSecondApplication = new TestApplication + { + ApplicationName = "Production-Test Application-Duplicated-Name", + ClaimSetName = "FakeClaimSet", + ProfileIds = null, + VendorId = secondVendor.VendorId + }; + secondResult = command.Execute(newSecondApplication, options); + }); + + Transaction(usersContext => + { + var persistedApplication = usersContext.Applications + .Include(a => a.ApplicationEducationOrganizations) + .Include(a => a.Vendor) + .Include(a => a.Profiles) + .Include(a => a.ApiClients).Where(a => a.ApplicationId == result.ApplicationId || a.ApplicationId == secondResult.ApplicationId).ToList(); + persistedApplication.TrueForAll(x => x.ApplicationName == "Production-Test Application-Duplicated-Name").ShouldBeTrue(); + persistedApplication.TrueForAll(x => x.ClaimSetName == "FakeClaimSet").ShouldBeTrue(); + persistedApplication.Count.ShouldBe(2); }); } @@ -252,8 +374,9 @@ private class TestApplication : IAddApplicationModel public string ApplicationName { get; set; } public int VendorId { get; set; } public string ClaimSetName { get; set; } - public int? ProfileId { get; set; } - public int? OdsInstanceId { get; set; } - public IEnumerable EducationOrganizationIds { get; set; } + public IEnumerable ProfileIds { get; set; } + public IEnumerable EducationOrganizationIds { get; set; } + public IEnumerable OdsInstanceIds { get; set; } + public bool? Enabled { get; set; } } } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceCommandTests.cs index bc6c546ad..63dc5abb0 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceCommandTests.cs @@ -1,46 +1,73 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using Moq; -using NUnit.Framework; -using Shouldly; -using System.Linq; - -namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; - -public class AddOdsInstanceCommandTests : PlatformUsersContextTestBase -{ - [Test] - public void ShouldAddOdsInstance() - { - var newOdsInstance = new Mock(); - newOdsInstance.Setup(x => x.Name).Returns("test ods instance"); - newOdsInstance.Setup(x => x.InstanceType).Returns("test type"); - newOdsInstance.Setup(x => x.Status).Returns("test status"); - newOdsInstance.Setup(x => x.IsExtended).Returns(true); - newOdsInstance.Setup(x => x.Version).Returns("test version"); - - var id = 0; - Transaction(usersContext => - { - var command = new AddOdsInstanceCommand(usersContext); - - id = command.Execute(newOdsInstance.Object).OdsInstanceId; - id.ShouldBeGreaterThan(0); - }); - - Transaction(usersContext => - { - var odsInstance = usersContext.OdsInstances - .Single(v => v.OdsInstanceId == id); - odsInstance.Name.ShouldBe("test ods instance"); - odsInstance.InstanceType.ShouldBe("test type"); - odsInstance.Status.ShouldBe("test status"); - odsInstance.IsExtended.ShouldBe(true); - odsInstance.Version.ShouldBe("test version"); - }); - } -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Linq; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Moq; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class AddOdsInstanceCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldAddOdsInstance() + { + var odsInstanceName = $"Test-OdsInstance{Guid.NewGuid()}"; + var odsInstanceType = $"Test-OdsInstanceType-{Guid.NewGuid()}"; + var odsInstanceConnectionString = "ConnectionString"; + var newOdsInstance = new Mock(); + newOdsInstance.Setup(x => x.Name).Returns(odsInstanceName); + newOdsInstance.Setup(x => x.InstanceType).Returns(odsInstanceType); + newOdsInstance.Setup(x => x.ConnectionString).Returns(odsInstanceConnectionString); + + + var id = 0; + Transaction(usersContext => + { + var command = new AddOdsInstanceCommand(usersContext); + id = command.Execute(newOdsInstance.Object).OdsInstanceId; + id.ShouldBeGreaterThan(0); + }); + + Transaction(usersContext => + { + var profile = usersContext.OdsInstances.Single(v => v.OdsInstanceId == id); + profile.Name.ShouldBe(odsInstanceName); + profile.InstanceType.ShouldBe(odsInstanceType); + profile.ConnectionString.ShouldBe(odsInstanceConnectionString); + }); + } + + [Test] + public void ShouldAddOdsInstanceWithEmptyInstanceType() + { + var odsInstanceName = $"Test-OdsInstance{Guid.NewGuid()}"; + var odsInstanceConnectionString = "ConnectionString"; + var newOdsInstance = new Mock(); + newOdsInstance.Setup(x => x.Name).Returns(odsInstanceName); + newOdsInstance.Setup(x => x.ConnectionString).Returns(odsInstanceConnectionString); + + + var id = 0; + Transaction(usersContext => + { + var command = new AddOdsInstanceCommand(usersContext); + id = command.Execute(newOdsInstance.Object).OdsInstanceId; + id.ShouldBeGreaterThan(0); + }); + + Transaction(usersContext => + { + var profile = usersContext.OdsInstances.Single(v => v.OdsInstanceId == id); + profile.Name.ShouldBe(odsInstanceName); + profile.InstanceType.ShouldBeNullOrEmpty(); + profile.ConnectionString.ShouldBe(odsInstanceConnectionString); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceContextTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceContextTests.cs new file mode 100644 index 000000000..bcca58c3b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceContextTests.cs @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + + +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Moq; +using NUnit.Framework; +using Shouldly; +using Microsoft.EntityFrameworkCore; +using System.Linq; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class AddOdsInstanceContextTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldAddOdsInstanceContext() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + Save(odsInstance); + + var contextKey = "contextKey"; + var contextValue = "contextValue"; + + var newOdsInstanceContext = new Mock(); + newOdsInstanceContext.Setup(x => x.OdsInstanceId).Returns(odsInstance.OdsInstanceId); + newOdsInstanceContext.Setup(x => x.ContextKey).Returns(contextKey); + newOdsInstanceContext.Setup(x => x.ContextValue).Returns(contextValue); + + var id = 0; + Transaction((usersContext) => + { + var command = new AddOdsInstanceContextCommand(usersContext); + id = command.Execute(newOdsInstanceContext.Object).OdsInstanceContextId; + id.ShouldBeGreaterThan(0); + }); + + Transaction((usersContext) => + { + var odsInstanceContext = usersContext.OdsInstanceContexts + .Include(o => o.OdsInstance) + .Single(v => v.OdsInstanceContextId == id); + + odsInstanceContext.OdsInstance.OdsInstanceId.ShouldBe(odsInstance.OdsInstanceId); + odsInstanceContext.ContextKey.ShouldBe(contextKey); + odsInstanceContext.ContextValue.ShouldBe(contextValue); + }); + } + + [Test] + public void ShouldFailOdsInstanceContextInvalidCombinedKey() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + Save(odsInstance); + + var contextKey = "contextKey"; + var contextValue = "contextValue"; + var newOdsInstanceContext = new Mock(); + newOdsInstanceContext.Setup(x => x.OdsInstanceId).Returns(odsInstance.OdsInstanceId); + newOdsInstanceContext.Setup(x => x.ContextKey).Returns(contextKey); + newOdsInstanceContext.Setup(x => x.ContextValue).Returns(contextValue); + var id = 0; + Transaction(usersContext => + { + var command = new AddOdsInstanceContextCommand(usersContext); + id = command.Execute(newOdsInstanceContext.Object).OdsInstanceContextId; + id.ShouldBeGreaterThan(0); + }); + + var newContextKey = "contextKey"; + var newContextValue = "contextValue"; + var newOdsInstanceContext2 = new Mock(); + newOdsInstanceContext2.Setup(x => x.OdsInstanceId).Returns(odsInstance.OdsInstanceId); + newOdsInstanceContext2.Setup(x => x.ContextKey).Returns(newContextKey); + newOdsInstanceContext2.Setup(x => x.ContextValue).Returns(newContextValue); + var newId = 0; + Assert.Throws(() => + { + Transaction(usersContext => + { + var command = new AddOdsInstanceContextCommand(usersContext); + newId = command.Execute(newOdsInstanceContext2.Object).OdsInstanceContextId; + newId.ShouldBeGreaterThan(0); + }); + }); + } + + + [Test] + public void ShouldFailToAddWithNullOrEmptyKey() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + Save(odsInstance); + + var contextValue = "2000"; + + var newOdsInstanceContext = new Mock(); + newOdsInstanceContext.Setup(x => x.OdsInstanceId).Returns(odsInstance.OdsInstanceId); + newOdsInstanceContext.Setup(x => x.ContextValue).Returns(contextValue); + + Assert.Throws(() => + { + Transaction(usersContext => + { + var command = new AddOdsInstanceContextCommand(usersContext); + var result = command.Execute(newOdsInstanceContext.Object); + }); + }); + } + + [Test] + public void ShouldFailToAddWithNullOrEmptyValue() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + Save(odsInstance); + + var contextKey = "School"; + + var newOdsInstanceContext = new Mock(); + newOdsInstanceContext.Setup(x => x.OdsInstanceId).Returns(odsInstance.OdsInstanceId); + newOdsInstanceContext.Setup(x => x.ContextKey).Returns(contextKey); + + Assert.Throws(() => + { + Transaction(usersContext => + { + var command = new AddOdsInstanceContextCommand(usersContext); + var result = command.Execute(newOdsInstanceContext.Object); + }); + }); + } + +} + + diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceDerivativeTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceDerivativeTests.cs new file mode 100644 index 000000000..7ca3d5131 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceDerivativeTests.cs @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + + +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Moq; +using NUnit.Framework; +using Shouldly; +using System.Linq; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class AddOdsInstanceDerivativeTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldAddOdsInstanceDerivative() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + Save(odsInstance); + + var derivativeType = "ReadReplica"; + var connectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False"; + + var newOdsInstanceDerivative = new Mock(); + newOdsInstanceDerivative.Setup(x => x.OdsInstanceId).Returns(odsInstance.OdsInstanceId); + newOdsInstanceDerivative.Setup(x => x.DerivativeType).Returns(derivativeType); + newOdsInstanceDerivative.Setup(x => x.ConnectionString).Returns(connectionString); + + var id = 0; + Transaction(usersContext => + { + var command = new AddOdsInstanceDerivativeCommand(usersContext); + id = command.Execute(newOdsInstanceDerivative.Object).OdsInstanceDerivativeId; + id.ShouldBeGreaterThan(0); + }); + + Transaction(usersContext => + { + var odsInstanceDerivative = usersContext.OdsInstanceDerivatives + .Include(o => o.OdsInstance) + .Single(v => v.OdsInstanceDerivativeId == id); + odsInstanceDerivative.OdsInstance.OdsInstanceId.ShouldBe(odsInstance.OdsInstanceId); + odsInstanceDerivative.DerivativeType.ShouldBe(derivativeType); + odsInstanceDerivative.ConnectionString.ShouldBe(connectionString); + }); + } + + [Test] + public void ShouldFailOdsInstanceDerivativeCombinedKey() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + Save(odsInstance); + + var derivativeType = "ReadReplica"; + var connectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False"; + var newOdsInstanceDerivative = new Mock(); + newOdsInstanceDerivative.Setup(x => x.OdsInstanceId).Returns(odsInstance.OdsInstanceId); + newOdsInstanceDerivative.Setup(x => x.DerivativeType).Returns(derivativeType); + newOdsInstanceDerivative.Setup(x => x.ConnectionString).Returns(connectionString); + var id = 0; + Transaction(usersContext => + { + var command = new AddOdsInstanceDerivativeCommand(usersContext); + id = command.Execute(newOdsInstanceDerivative.Object).OdsInstanceDerivativeId; + id.ShouldBeGreaterThan(0); + }); + + var newDerivativeType = "ReadReplica"; + var newOdsInstanceDerivative2 = new Mock(); + newOdsInstanceDerivative2.Setup(x => x.OdsInstanceId).Returns(odsInstance.OdsInstanceId); + newOdsInstanceDerivative2.Setup(x => x.DerivativeType).Returns(newDerivativeType); + newOdsInstanceDerivative2.Setup(x => x.ConnectionString).Returns(connectionString); + var newId = 0; + Assert.Throws(() => + { + Transaction(usersContext => + { + var command = new AddOdsInstanceDerivativeCommand(usersContext); + newId = command.Execute(newOdsInstanceDerivative2.Object).OdsInstanceDerivativeId; + newId.ShouldBeGreaterThan(0); + }); + }); + + } + + [Test] + [Ignore("Column is allowing null values")] + public void ShouldFailToAddWhenConnectionStringIsEmpty() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + Save(odsInstance); + + var derivativeType = "ReadReplica"; + + var newOdsInstanceDerivative = new Mock(); + newOdsInstanceDerivative.Setup(x => x.OdsInstanceId).Returns(odsInstance.OdsInstanceId); + newOdsInstanceDerivative.Setup(x => x.DerivativeType).Returns(derivativeType); + + var id = 0; + Assert.Throws(() => + { + Transaction(usersContext => + { + var command = new AddOdsInstanceDerivativeCommand(usersContext); + id = command.Execute(newOdsInstanceDerivative.Object).OdsInstanceDerivativeId; + }); + }); + Assert.That(id, Is.EqualTo(0)); + } + + [Test] + public void ShouldFailToAddWhenDerivativeTypeIsEmpty() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + Save(odsInstance); + + var connectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False"; + + var newOdsInstanceDerivative = new Mock(); + newOdsInstanceDerivative.Setup(x => x.OdsInstanceId).Returns(odsInstance.OdsInstanceId); + newOdsInstanceDerivative.Setup(x => x.ConnectionString).Returns(connectionString); + + var id = 0; + Assert.Throws(() => + { + Transaction(usersContext => + { + var command = new AddOdsInstanceDerivativeCommand(usersContext); + id = command.Execute(newOdsInstanceDerivative.Object).OdsInstanceDerivativeId; + }); + }); + Assert.That(id, Is.EqualTo(0)); + } + +} + + diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddProfileCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddProfileCommandTests.cs new file mode 100644 index 000000000..0f86464e3 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddProfileCommandTests.cs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + + +using System.Linq; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Moq; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class AddProfileCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldAddProfile() + { + var profileName = "Test-Profile"; + var newProfile = new Mock(); + newProfile.Setup(x => x.Name).Returns(profileName); + var definition = "" + + "" + + "" + + "" + + "" + + ""; + newProfile.Setup(x => x.Definition).Returns(definition); + + var id = 0; + Transaction(usersContext => + { + var command = new AddProfileCommand(usersContext); + id = command.Execute(newProfile.Object).ProfileId; + id.ShouldBeGreaterThan(0); + }); + + Transaction(usersContext => + { + var profile = usersContext.Profiles.Single(v => v.ProfileId == id); + profile.ProfileName.ShouldBe(profileName); + profile.ProfileDefinition.ShouldBe(definition); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddVendorCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddVendorCommandTests.cs index 3b91a58e3..516538e00 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddVendorCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddVendorCommandTests.cs @@ -8,9 +8,9 @@ using Moq; using NUnit.Framework; using Shouldly; -using System.Linq; -using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using System.Linq; using Microsoft.EntityFrameworkCore; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; @@ -37,10 +37,9 @@ public void ShouldAddVendor() Transaction(usersContext => { - var vendor = usersContext.Vendors - .Include(x => x.VendorNamespacePrefixes) - .Include(x => x.Users) - .Single(v => v.VendorId == id); + var vendor = usersContext.Vendors + .Include(v => v.VendorNamespacePrefixes) + .Include(v => v.Users).Single(v => v.VendorId == id); vendor.VendorName.ShouldBe("test vendor"); vendor.VendorNamespacePrefixes.First().NamespacePrefix.ShouldBe("http://www.test.com/"); vendor.Users.Single().FullName.ShouldBe("test user"); @@ -74,9 +73,9 @@ public void ShouldAddVendorIfMultipleNamespacePrefixes() Transaction(usersContext => { - var vendor = usersContext.Vendors - .Include(x => x.VendorNamespacePrefixes) - .Include(x => x.Users).Single(v => v.VendorId == id); + var vendor = usersContext.Vendors + .Include(v => v.VendorNamespacePrefixes) + .Include(v => v.Users).Single(v => v.VendorId == id); vendor.VendorName.ShouldBe("test vendor"); vendor.VendorNamespacePrefixes.Select(x => x.NamespacePrefix).ShouldBe(namespacePrefixes); vendor.Users.Single().FullName.ShouldBe("test user"); @@ -107,10 +106,10 @@ public void ShouldNotAddEmptyNamespacePrefixesWhileAddingVendor(string inputName Transaction(usersContext => { - var vendor = usersContext.Vendors. - Include(x => x.VendorNamespacePrefixes) - .Include(x => x.Users) - .Single(v => v.VendorId == id); + var vendor = usersContext.Vendors + .Include(v => v.VendorNamespacePrefixes) + .Include(v => v.Users) + .Single(v => v.VendorId == id); vendor.VendorName.ShouldBe("test vendor"); vendor.VendorNamespacePrefixes.Select(x => x.NamespacePrefix).ToDelimiterSeparated().ShouldBe(expectedNamespacePrefixes); vendor.Users.Single().FullName.ShouldBe("test user"); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteApiClientCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteApiClientCommandTests.cs new file mode 100644 index 000000000..e2dc9b036 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteApiClientCommandTests.cs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +internal class DeleteApiClientCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldDeleteApiClient() + { + var apiClient = new ApiClient(true) + { + Name = "Test API Client", + IsApproved = true, + Application = new Application + { + ApplicationName = "Test Application", + ClaimSetName = "FakeClaimSet", + OperationalContextUri = "http://test.com", + Profiles = null, + Vendor = new Vendor + { + VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://tests.com" } }, + VendorName = "Integration Tests" + } + } + }; + Save(apiClient); + + Transaction(usersContext => + { + var deleteApplicationCommand = new DeleteApiClientCommand(usersContext); + deleteApplicationCommand.Execute(apiClient.ApiClientId); + }); + + Transaction(usersContext => usersContext.ApiClients.Where(a => a.ApiClientId == apiClient.ApiClientId).ToArray()).ShouldBeEmpty(); + } + + [Test] + public void ShouldDeleteApiClientWithOdsInstances() + { + var apiClient = new ApiClient(true) + { + Name = "Test API Client", + IsApproved = true, + Application = new Application + { + ApplicationName = "Test Application", + ClaimSetName = "FakeClaimSet", + OperationalContextUri = "http://test.com", + Profiles = null, + Vendor = new Vendor + { + VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://tests.com" } }, + VendorName = "Integration Tests" + } + } + }; + + var odsInstance = new OdsInstance + { + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + + var apiClientOdsInstance = new ApiClientOdsInstance + { + OdsInstance = odsInstance, + ApiClient = apiClient + }; + + + Save(odsInstance, apiClient, apiClientOdsInstance); + + Transaction(usersContext => + { + var deleteApplicationCommand = new DeleteApiClientCommand(usersContext); + deleteApplicationCommand.Execute(apiClient.ApiClientId); + }); + + Transaction(usersContext => usersContext.ApiClients.Where(a => a.ApiClientId == apiClient.ApiClientId).ToArray()).ShouldBeEmpty(); + + Transaction(usersContext => usersContext.ApiClientOdsInstances.Where(a => a.ApiClient.ApiClientId == apiClient.ApiClientId).ToArray()).ShouldBeEmpty(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteApplicationCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteApplicationCommandTests.cs index 11a09cada..967dab7af 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteApplicationCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteApplicationCommandTests.cs @@ -3,14 +3,15 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Microsoft.EntityFrameworkCore; using NUnit.Framework; using Shouldly; using System; -using System.Collections.Generic; using System.Linq; -using EdFi.Admin.DataAccess.Models; -using EdFi.Ods.AdminApi.Infrastructure; namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; @@ -46,6 +47,15 @@ public void ShouldDeleteApplicationWithClient() ActivationCode = "fake activation code" }; + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name Test", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + Save(odsInstance); + var clientAccessToken = new ClientAccessToken { ApiClient = client, @@ -57,6 +67,18 @@ public void ShouldDeleteApplicationWithClient() application.ApiClients.Add(client); Save(application); + Transaction(usersContext => + { + var createdOdsInstance = usersContext.OdsInstances.FirstOrDefault(odsI => odsI.OdsInstanceId == odsInstance.OdsInstanceId); + var createdApiClient = usersContext.ApiClients.FirstOrDefault(apiClient => apiClient.ApiClientId == client.ApiClientId); + usersContext.ApiClientOdsInstances.Add(new ApiClientOdsInstance + { + OdsInstance = createdOdsInstance, + ApiClient = createdApiClient + }); + usersContext.SaveChanges(); + }); + var applicationId = application.ApplicationId; applicationId.ShouldBeGreaterThan(0); @@ -72,8 +94,17 @@ public void ShouldDeleteApplicationWithClient() deleteApplicationCommand.Execute(applicationId); }); - Transaction(usersContext => usersContext.Applications.Where(a => a.ApplicationId == applicationId).ToArray()).ShouldBeEmpty(); - Transaction(usersContext => usersContext.Clients.Where(c => c.ApiClientId == clientId).ToArray()).ShouldBeEmpty(); + Transaction(usersContext => + { + var application = usersContext.Applications + .Include(a => a.ApiClients).FirstOrDefault(a => a.ApplicationId == applicationId); + application.ShouldBeNull(); + var apiClientOdsInstances = usersContext.ApiClientOdsInstances + .Include(p => p.OdsInstance).Where(c => c.ApiClient.ApiClientId == clientId && c.OdsInstance.OdsInstanceId == odsInstance.OdsInstanceId).ToArray(); + apiClientOdsInstances.ShouldBeEmpty(); + var apiClients = usersContext.ApiClients.Where(c => c.ApiClientId == clientId).ToArray(); + apiClients.ShouldBeEmpty(); + }); } [Test] @@ -91,9 +122,8 @@ public void ShouldDeleteApplicationWithOrganization() var organization = new ApplicationEducationOrganization { Application = application, - Clients = new List { client } }; - + organization.ApiClients.Add(client); application.ApiClients.Add(client); application.ApplicationEducationOrganizations.Add(organization); Save(application); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceCommandTests.cs index 065728883..8b5e5c503 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceCommandTests.cs @@ -1,38 +1,39 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using NUnit.Framework; -using Shouldly; -using System.Linq; -using EdFi.Admin.DataAccess.Models; - -namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; - -[TestFixture] -public class DeleteOdsInstanceCommandTests : PlatformUsersContextTestBase -{ - [Test] - public void ShouldDeleteOdsInstance() - { - var newOdsInstance = new OdsInstance() - { - Name = "test", - InstanceType = "type", - Status = "status", - Version = "version" - }; - Save(newOdsInstance); - var odsInstanceId = newOdsInstance.OdsInstanceId; - - Transaction(usersContext => - { - var deleteOdsInstanceCommand = new DeleteOdsInstanceCommand(usersContext); - deleteOdsInstanceCommand.Execute(odsInstanceId); - }); - - Transaction(usersContext => usersContext.OdsInstances.Where(v => v.OdsInstanceId == odsInstanceId).ToArray()).ShouldBeEmpty(); - } -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using NUnit.Framework; +using Shouldly; +using System.Linq; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class DeleteOdsInstanceCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldDeleteOdsInstance() + { + var newOdsInstance = + new OdsInstance() + { + Name = "Name Test", + ConnectionString = "Connection String Test", + InstanceType = "Instance Type Test" + }; + + Save(newOdsInstance); + var odsInstanceId = newOdsInstance.OdsInstanceId; + + Transaction(usersContext => + { + var deleteOdsInstanceCommand = new DeleteOdsInstanceCommand(usersContext); + deleteOdsInstanceCommand.Execute(odsInstanceId); + }); + + Transaction(usersContext => usersContext.OdsInstances.Where(oi => oi.OdsInstanceId == odsInstanceId).ToArray()).ShouldBeEmpty(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceContextTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceContextTests.cs new file mode 100644 index 000000000..494197098 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceContextTests.cs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using NUnit.Framework; +using Shouldly; +using System.Linq; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class DeleteOdsInstanceContextTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldDeleteOdsInstanceContext() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var newOdsInstanceContext = new OdsInstanceContext() + { + ContextKey = "contextKey", + ContextValue = "contextValue", + OdsInstance = odsInstance + }; + Save(newOdsInstanceContext); + var odsInstanceContextId = newOdsInstanceContext.OdsInstanceContextId; + + Transaction(usersContext => + { + var deleteOdsInstanceContextCommand = new DeleteOdsInstanceContextCommand(usersContext); + deleteOdsInstanceContextCommand.Execute(odsInstanceContextId); + }); + + Transaction(usersContext => usersContext.OdsInstanceContexts.Where(v => v.OdsInstanceContextId == odsInstanceContextId).ToArray()).ShouldBeEmpty(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceDerivativeTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceDerivativeTests.cs new file mode 100644 index 000000000..301dfa557 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceDerivativeTests.cs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using NUnit.Framework; +using Shouldly; +using System.Linq; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class DeleteOdsInstanceDerivativeTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldDeleteOdsInstanceDerivative() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var newOdsInstanceDerivative = new OdsInstanceDerivative() + { + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False", + DerivativeType = "ReadReplica", + OdsInstance = odsInstance + }; + Save(newOdsInstanceDerivative); + var odsInstanceDerivativeId = newOdsInstanceDerivative.OdsInstanceDerivativeId; + + Transaction(usersContext => + { + var deleteOdsInstanceDerivativeCommand = new DeleteOdsInstanceDerivativeCommand(usersContext); + deleteOdsInstanceDerivativeCommand.Execute(odsInstanceDerivativeId); + }); + + Transaction(usersContext => usersContext.OdsInstanceDerivatives.Where(v => v.OdsInstanceDerivativeId == odsInstanceDerivativeId).ToArray()).ShouldBeEmpty(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteProfileCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteProfileCommandTests.cs new file mode 100644 index 000000000..6f3264754 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteProfileCommandTests.cs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using NUnit.Framework; +using Shouldly; +using System.Linq; +using EdFi.Admin.DataAccess.Models; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class DeleteProfileCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldDeleteProfile() + { + var newProfile = new Profile() + { + ProfileName = "Test" + }; + Save(newProfile); + var profileId = newProfile.ProfileId; + + Transaction(usersContext => + { + var deleteProfileCommand = new DeleteProfileCommand(usersContext); + deleteProfileCommand.Execute(profileId); + }); + + Transaction(usersContext => usersContext.Profiles.Where(v => v.ProfileId == profileId).ToArray()).ShouldBeEmpty(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteVendorCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteVendorCommandTests.cs index 8261bb266..839245693 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteVendorCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteVendorCommandTests.cs @@ -10,6 +10,7 @@ using EdFi.Admin.DataAccess.Models; using VendorUser = EdFi.Admin.DataAccess.Models.User; using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure; namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditApiClientCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditApiClientCommandTests.cs new file mode 100644 index 000000000..17ccb2d0c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditApiClientCommandTests.cs @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; +using VendorUser = EdFi.Admin.DataAccess.Models.User; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +internal class EditApiClientCommandTests : PlatformUsersContextTestBase +{ + private Vendor _vendor; + private VendorUser _user; + private ApiClient _apiClient; + private Application _application; + private OdsInstance _odsInstance; + private ApiClientOdsInstance _apiClientOdsInstance; + + private IOptions _options { get; set; } + + [SetUp] + public new virtual async Task SetUp() + { + AppSettings appSettings = new() + { + PreventDuplicateApplications = false + }; + _options = Options.Create(appSettings); + await Task.Yield(); + } + + private void SetupTestEntities() + { + _odsInstance = new OdsInstance + { + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + _vendor = new Vendor + { + VendorNamespacePrefixes = [new() { NamespacePrefix = "http://tests.com" }], + VendorName = "Integration Tests" + }; + + _user = new VendorUser + { + Email = "nobody@nowhere.com", + FullName = "Integration Tests", + Vendor = _vendor + }; + + _apiClient = new ApiClient(true) + { + Name = "Integration Test", + IsApproved = true, + UseSandbox = false, + }; + + _apiClientOdsInstance = new ApiClientOdsInstance + { + OdsInstance = _odsInstance, + ApiClient = _apiClient + }; + + _application = new Application + { + ApplicationName = "Test Application", + ClaimSetName = "FakeClaimSet", + Vendor = _vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + + _application.ApiClients.Add(_apiClient); + + Save(_vendor, _user, _application, _odsInstance, _apiClientOdsInstance); + } + + [Test] + public void ShouldEditName() + { + SetupTestEntities(); + + var editModel = new TestEditApplicationModel + { + Id = _apiClient.ApiClientId, + ApplicationId = _application.ApplicationId, + Name = $"{_apiClient.Name}_Edited", + IsApproved = true, + OdsInstanceIds = null + }; + + Transaction(usersContext => + { + var command = new EditApiClientCommand(usersContext); + command.Execute(editModel); + }); + + Transaction(usersContext => + { + var apiClientOdsInstances = usersContext.ApiClients.First(ac => ac.ApiClientId == _apiClient.ApiClientId); + apiClientOdsInstances.Name.ShouldBe($"{_apiClient.Name}_Edited"); + }); + } + + [Test] + public void ShouldEditIsApproved() + { + SetupTestEntities(); + + var editModel = new TestEditApplicationModel + { + Id = _apiClient.ApiClientId, + ApplicationId = _application.ApplicationId, + Name = _apiClient.Name, + IsApproved = false, + OdsInstanceIds = null + }; + + Transaction(usersContext => + { + var command = new EditApiClientCommand(usersContext); + command.Execute(editModel); + }); + + Transaction(usersContext => + { + var apiClientOdsInstances = usersContext.ApiClients.First(ac => ac.ApiClientId == _apiClient.ApiClientId); + apiClientOdsInstances.IsApproved.ShouldBe(false); + }); + } + + [Test] + public void ShouldRemoveOdsInstancesIfNull() + { + SetupTestEntities(); + + var editModel = new TestEditApplicationModel + { + Id = _apiClient.ApiClientId, + ApplicationId = _application.ApplicationId, + Name = _apiClient.Name, + IsApproved = true, + OdsInstanceIds = null + }; + + Transaction(usersContext => + { + var command = new EditApiClientCommand(usersContext); + command.Execute(editModel); + }); + + Transaction(usersContext => + { + var apiClientOdsInstances = usersContext.ApiClientOdsInstances.Where(oi => oi.ApiClient.ApiClientId == _apiClient.ApiClientId); + apiClientOdsInstances.ShouldBeEmpty(); + }); + } + + [Test] + public void ShouldAddOdsInstancesIfNew() + { + SetupTestEntities(); + + var _newOdsInstance = new OdsInstance + { + Name = "Other Test Instance", + InstanceType = "other Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + Save(_newOdsInstance); + + var editModel = new TestEditApplicationModel + { + Id = _apiClient.ApiClientId, + ApplicationId = _application.ApplicationId, + Name = _apiClient.Name, + IsApproved = true, + OdsInstanceIds = [_odsInstance.OdsInstanceId, _newOdsInstance.OdsInstanceId] + }; + + Transaction(usersContext => + { + var command = new EditApiClientCommand(usersContext); + command.Execute(editModel); + }); + + Transaction(usersContext => + { + var apiClientOdsInstances = usersContext.ApiClientOdsInstances.Where(oi => oi.ApiClient.ApiClientId == _apiClient.ApiClientId); + apiClientOdsInstances.Count().ShouldBe(2); + }); + } + private class TestEditApplicationModel : IEditApiClientModel + { + public int Id { get; set; } + public string Name { get; set; } + public bool IsApproved { get; set; } + public int ApplicationId { get; set; } + public IEnumerable OdsInstanceIds { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditApplicationCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditApplicationCommandTests.cs index 4555e89fa..a405e29e4 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditApplicationCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditApplicationCommandTests.cs @@ -3,28 +3,23 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System.Collections.Generic; -using System.Linq; using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Microsoft.EntityFrameworkCore; using NUnit.Framework; using Shouldly; -using VendorUser = EdFi.Admin.DataAccess.Models.User; -using EdFi.Ods.AdminApi.Infrastructure; +using System.Collections.Generic; +using System.Linq; using Profile = EdFi.Admin.DataAccess.Models.Profile; -using System.Threading.Tasks; -using System.Data.SqlClient; -using Microsoft.EntityFrameworkCore; - +using VendorUser = EdFi.Admin.DataAccess.Models.User; + namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; [TestFixture] public class EditApplicationCommandTests : PlatformUsersContextTestBase { - private const int EdOrgId1 = 1234; - private const int EdOrgId2 = 2345; - private const int EdOrgId3 = 56666; - private Vendor _vendor; private Vendor _otherVendor; private VendorUser _user; @@ -33,10 +28,18 @@ public class EditApplicationCommandTests : PlatformUsersContextTestBase private Profile _otherProfile; private ApiClient _apiClient; private Application _application; + private OdsInstance _odsInstance; private void SetupTestEntities() { + _odsInstance = new OdsInstance + { + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + _vendor = new Vendor { VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://tests.com" } }, @@ -89,10 +92,10 @@ private void SetupTestEntities() _application.ApiClients.Add(_apiClient); _application.Profiles.Add(_profile); - _application.ApplicationEducationOrganizations.Add(_application.CreateApplicationEducationOrganization(EdOrgId1)); - _application.ApplicationEducationOrganizations.Add(_application.CreateApplicationEducationOrganization(EdOrgId2)); + _application.ApplicationEducationOrganizations.Add(_application.CreateApplicationEducationOrganization(12345)); + _application.ApplicationEducationOrganizations.Add(_application.CreateApplicationEducationOrganization(67890)); - Save(_vendor, _otherVendor, _user, _otherUser, _profile, _otherProfile, _application); + Save(_vendor, _otherVendor, _user, _otherUser, _profile, _otherProfile, _application, _odsInstance); } [Test] @@ -102,12 +105,13 @@ public void ShouldRemoveProfileIfNull() var editModel = new TestEditApplicationModel { - ApplicationId = _application.ApplicationId, + Id = _application.ApplicationId, ApplicationName = _application.ApplicationName, ClaimSetName = _application.ClaimSetName, - EducationOrganizationIds = new List { EdOrgId1, EdOrgId2 }, - ProfileId = null, - VendorId = _vendor.VendorId + EducationOrganizationIds = new List { 12345, 67890 }, + ProfileIds = null, + VendorId = _vendor.VendorId, + OdsInstanceIds = new List { _odsInstance.OdsInstanceId } }; Transaction(usersContext => @@ -118,10 +122,10 @@ public void ShouldRemoveProfileIfNull() Transaction(usersContext => { - var persistedApplication = usersContext.Applications - .Include(x => x.ApiClients) - .Include(x => x.ApplicationEducationOrganizations) - .Include(x => x.Profiles) + var persistedApplication = usersContext.Applications + .Include(a => a.ApiClients) + .Include(a => a.Profiles) + .Include(a => a.ApplicationEducationOrganizations) .Single(a => a.ApplicationId == _application.ApplicationId); persistedApplication.ApplicationName.ShouldBe("Test Application"); persistedApplication.ClaimSetName.ShouldBe("FakeClaimSet"); @@ -129,7 +133,7 @@ public void ShouldRemoveProfileIfNull() persistedApplication.ApiClients.First().Name.ShouldBe("Test Application"); persistedApplication.ApiClients.First().ApplicationEducationOrganizations.ShouldAllBe(aeo => persistedApplication.ApplicationEducationOrganizations.Contains(aeo)); persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(2); - persistedApplication.ApplicationEducationOrganizations.ShouldAllBe(aeo => aeo.EducationOrganizationId == EdOrgId1 || aeo.EducationOrganizationId == EdOrgId2); + persistedApplication.ApplicationEducationOrganizations.ShouldAllBe(aeo => aeo.EducationOrganizationId == 12345 || aeo.EducationOrganizationId == 67890); persistedApplication.Profiles.Count.ShouldBe(0); }); } @@ -141,12 +145,13 @@ public void ShouldUpdateAllEntitiesProperly() var editModel = new TestEditApplicationModel { - ApplicationId = _application.ApplicationId, + Id = _application.ApplicationId, ApplicationName = "New Application Name", ClaimSetName = "DifferentFakeClaimSet", - EducationOrganizationIds = new List { EdOrgId2, EdOrgId3 }, - ProfileId = _otherProfile.ProfileId, - VendorId = _otherVendor.VendorId + EducationOrganizationIds = new List { 23456, 78901, 5000000005 }, + ProfileIds = new List() { _otherProfile.ProfileId }, + VendorId = _otherVendor.VendorId, + OdsInstanceIds = new List { _odsInstance.OdsInstanceId } }; Transaction(usersContext => @@ -157,11 +162,10 @@ public void ShouldUpdateAllEntitiesProperly() Transaction(usersContext => { - var persistedApplication = usersContext.Applications - .Include(x => x.ApiClients) - .Include(x => x.ApplicationEducationOrganizations) - .Include(x => x.Profiles) - .Single(a => a.ApplicationId == _application.ApplicationId); + var persistedApplication = usersContext.Applications + .Include(a => a.ApiClients) + .Include(a => a.Profiles) + .Include(a => a.ApplicationEducationOrganizations).Single(a => a.ApplicationId == _application.ApplicationId); persistedApplication.ApplicationName.ShouldBe("New Application Name"); persistedApplication.ClaimSetName.ShouldBe("DifferentFakeClaimSet"); persistedApplication.ApiClients.Count.ShouldBe(1); @@ -169,97 +173,38 @@ public void ShouldUpdateAllEntitiesProperly() persistedApplication.ApiClients.First().ApplicationEducationOrganizations.ShouldAllBe(aeo => persistedApplication.ApplicationEducationOrganizations.Contains(aeo)); persistedApplication.Profiles.Count.ShouldBe(1); persistedApplication.Profiles.First().ProfileName.ShouldBe("Other Test Profile"); - persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(2); - persistedApplication.ApplicationEducationOrganizations.ShouldAllBe(aeo => aeo.EducationOrganizationId == EdOrgId2 || aeo.EducationOrganizationId == EdOrgId3); + persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(3); + persistedApplication.ApplicationEducationOrganizations.ShouldAllBe(aeo => aeo.EducationOrganizationId == 23456 || aeo.EducationOrganizationId == 78901 || aeo.EducationOrganizationId == 5000000005); }); - } - - [Test] - public void GivenAdditionalEdOrgThenItShouldBeConnectedToAllThreeEdOrgIds() - { - // Arrange - SetupTestEntities(); - - // Act - var edOrgs = _application.ApplicationEducationOrganizations.Select(x => x.EducationOrganizationId).ToList().Append(EdOrgId3); - - var editApplication = new TestEditApplicationModel - { - ApplicationId = _application.ApplicationId, - ApplicationName = _application.ApplicationName, - ClaimSetName = _application.ClaimSetName, - EducationOrganizationIds = edOrgs, - ProfileId = _application.Profiles.FirstOrDefault()?.ProfileId, - VendorId = _application.Vendor.VendorId - }; - - Transaction(usersContext => - { - var command = new EditApplicationCommand(usersContext); - command.Execute(editApplication); - }); - // Assert - Transaction(UsersContext => - { - var aeos = UsersContext.ApplicationEducationOrganizations.ToList(); - aeos.Count.ShouldBe(3); - aeos.ShouldContain(x => x.EducationOrganizationId == EdOrgId1); - aeos.ShouldContain(x => x.EducationOrganizationId == EdOrgId2); - aeos.ShouldContain(x => x.EducationOrganizationId == EdOrgId3); - }); - } - - [Test] - public async Task GivenChangedEdOrgIdThenItShouldBeConnectedToOnlyTheOneEdOrgid() + Transaction(usersContext => { - // Arrange - SetupTestEntities(); - - // Act - var editApplication = new TestEditApplicationModel - { - ApplicationId = _application.ApplicationId, - ApplicationName = _application.ApplicationName, - ClaimSetName = _application.ClaimSetName, - // Now connected to just one - EducationOrganizationIds = new List { EdOrgId3 }, - ProfileId = _application.Profiles.FirstOrDefault()?.ProfileId, - VendorId = _application.Vendor.VendorId - }; - - Transaction(usersContext => - { - var command = new EditApplicationCommand(usersContext); - command.Execute(editApplication); - }); + var persistedApplication = usersContext.Applications + .Include(a => a.ApiClients) + .Include(a => a.Profiles) + .Include(a => a.ApplicationEducationOrganizations).Single(a => a.ApplicationId == _application.ApplicationId); + var apiClient = persistedApplication.ApiClients.First(); + var odsInstanceId = _odsInstance.OdsInstanceId; + var apiClientOdsInstance = usersContext.ApiClientOdsInstances + .Include(a => a.OdsInstance) + .Include(a => a.ApiClient) + .FirstOrDefault(o => o.OdsInstance.OdsInstanceId == odsInstanceId && o.ApiClient.ApiClientId == apiClient.ApiClientId); + apiClientOdsInstance.ApiClient.ApiClientId.ShouldBe(apiClient.ApiClientId); + apiClientOdsInstance.OdsInstance.OdsInstanceId.ShouldBe(_odsInstance.OdsInstanceId); + }); - // Assert - Transaction(usersContext => - { - var aeos = usersContext.ApplicationEducationOrganizations.ToList(); - aeos.Count.ShouldBe(1); - var first = aeos.First(); - first.EducationOrganizationId.ShouldBe(EdOrgId3); - }); - // Not trusting Entity Framework for the following check - directly querying the database - const string sql = "select count(1) from dbo.ApiClientApplicationEducationOrganizations"; - using var connection = new SqlConnection(ConnectionString); - await connection.OpenAsync(); - using var command = new SqlCommand(sql, connection); - var count = (int)await command.ExecuteScalarAsync(); - count.ShouldBe(1); - } + } private class TestEditApplicationModel : IEditApplicationModel { - public int ApplicationId { get; set; } + public int Id { get; set; } public string ApplicationName { get; set; } public int VendorId { get; set; } public string ClaimSetName { get; set; } - public int? ProfileId { get; set; } - public int? OdsInstanceId { get; set; } - public IEnumerable EducationOrganizationIds { get; set; } + public IEnumerable ProfileIds { get; set; } + public IEnumerable EducationOrganizationIds { get; set; } + public IEnumerable OdsInstanceIds { get; set; } + public bool? Enabled { get; set; } } } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditOdsInstanceCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditOdsInstanceCommandTests.cs index c683b8074..312f0c79f 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditOdsInstanceCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditOdsInstanceCommandTests.cs @@ -3,17 +3,17 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; using Moq; using NUnit.Framework; using Shouldly; using System.Linq; -using EdFi.Admin.DataAccess.Models; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; [TestFixture] -internal class EditOdsInstanceCommandTests : PlatformUsersContextTestBase +public class EditOdsInstanceCommandTests : PlatformUsersContextTestBase { private int _odsInstanceId; @@ -22,27 +22,52 @@ public void Init() { var originalOdsInstance = new OdsInstance { - Name = "old ods instance name", - InstanceType = "old type", - Status = "old status", - IsExtended = false, - Version = "old version", + Name = "old odsinstance name", + InstanceType = "old odsinstance instance type", + ConnectionString = "old odsinstance connection string", }; + Save(originalOdsInstance); _odsInstanceId = originalOdsInstance.OdsInstanceId; } [Test] - public void ShouldEditVendor() + public void ShouldEditOdsInstance() + { + var name = "new odsinstance name"; + var instanceType = "new odsinstance instance type"; + var connectionString = "new odsinstance connection string"; + var newOdsInstanceData = new Mock(); + newOdsInstanceData.Setup(v => v.Id).Returns(_odsInstanceId); + newOdsInstanceData.Setup(v => v.Name).Returns(name); + newOdsInstanceData.Setup(v => v.InstanceType).Returns(instanceType); + newOdsInstanceData.Setup(v => v.ConnectionString).Returns(connectionString); + + Transaction(usersContext => + { + var editOdsInstanceCommand = new EditOdsInstanceCommand(usersContext); + editOdsInstanceCommand.Execute(newOdsInstanceData.Object); + }); + + Transaction(usersContext => + { + var changedOdsInstance = usersContext.OdsInstances.Single(v => v.OdsInstanceId == _odsInstanceId); + changedOdsInstance.Name.ShouldBe(name); + changedOdsInstance.InstanceType.ShouldBe(instanceType); + changedOdsInstance.ConnectionString.ShouldBe(connectionString); + }); + } + + [Test] + public void ShouldEditOdsInstanceWithEmptyInstanceType() { + var name = "new odsinstance name"; + var connectionString = "new odsinstance connection string"; var newOdsInstanceData = new Mock(); - newOdsInstanceData.Setup(v => v.OdsInstanceId).Returns(_odsInstanceId); - newOdsInstanceData.Setup(v => v.Name).Returns("new ods instance name"); - newOdsInstanceData.Setup(v => v.InstanceType).Returns("new type"); - newOdsInstanceData.Setup(v => v.Status).Returns("new status"); - newOdsInstanceData.Setup(v => v.IsExtended).Returns(true); - newOdsInstanceData.Setup(v => v.Version).Returns("new version"); + newOdsInstanceData.Setup(v => v.Id).Returns(_odsInstanceId); + newOdsInstanceData.Setup(v => v.Name).Returns(name); + newOdsInstanceData.Setup(v => v.ConnectionString).Returns(connectionString); Transaction(usersContext => { @@ -52,13 +77,10 @@ public void ShouldEditVendor() Transaction(usersContext => { - var changedOdsInstance = usersContext.OdsInstances - .Single(v => v.OdsInstanceId == _odsInstanceId); - changedOdsInstance.Name.ShouldBe("new ods instance name"); - changedOdsInstance.InstanceType.ShouldBe("new type"); - changedOdsInstance.Status.ShouldBe("new status"); - changedOdsInstance.IsExtended.ShouldBe(true); - changedOdsInstance.Version.ShouldBe("new version"); + var changedOdsInstance = usersContext.OdsInstances.Single(v => v.OdsInstanceId == _odsInstanceId); + changedOdsInstance.Name.ShouldBe(name); + changedOdsInstance.InstanceType.ShouldBeNullOrEmpty(); + changedOdsInstance.ConnectionString.ShouldBe(connectionString); }); } } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditOdsInstanceContextTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditOdsInstanceContextTests.cs new file mode 100644 index 000000000..2ed69faf3 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditOdsInstanceContextTests.cs @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Moq; +using NUnit.Framework; +using Shouldly; +using Microsoft.EntityFrameworkCore; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class EditOdsInstanceContextTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldEditOdsInstanceContext() + { + var odsInstance1 = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods1", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var odsInstance2 = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods2", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + Save(odsInstance2); + + var contextKey = "contextKey"; + var contextValue = "contextValue"; + var newOdsInstanceContext = new OdsInstanceContext + { + ContextKey = contextKey, + ContextValue = contextValue, + OdsInstance = odsInstance1 + }; + Save(newOdsInstanceContext); + + var updateContextKey = "contextKey2"; + var updateContextValue = "contextValue2"; + var editOdsInstanceContext = new Mock(); + editOdsInstanceContext.Setup(x => x.OdsInstanceId).Returns(odsInstance2.OdsInstanceId); + editOdsInstanceContext.Setup(x => x.ContextKey).Returns(updateContextKey); + editOdsInstanceContext.Setup(x => x.ContextValue).Returns(updateContextValue); + editOdsInstanceContext.Setup(x => x.Id).Returns(newOdsInstanceContext.OdsInstanceContextId); + + Transaction(usersContext => + { + var command = new EditOdsInstanceContextCommand(usersContext); + var updatedOdsInstanceContext = command.Execute(editOdsInstanceContext.Object); + updatedOdsInstanceContext.ShouldNotBeNull(); + updatedOdsInstanceContext.OdsInstanceContextId.ShouldBeGreaterThan(0); + updatedOdsInstanceContext.OdsInstanceContextId.ShouldBe(newOdsInstanceContext.OdsInstanceContextId); + updatedOdsInstanceContext.OdsInstance.OdsInstanceId.ShouldBe(odsInstance2.OdsInstanceId); + updatedOdsInstanceContext.ContextKey.ShouldBe(updateContextKey); + updatedOdsInstanceContext.ContextValue.ShouldBe(updateContextValue); + }); + } + + + [Test] + public void ShouldFailOdsInstanceContextCombinedKey() + { + var odsInstance1 = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods1", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + var contextKey = "contextKey"; + var contextValue = "contextValue"; + var newOdsInstanceContext = new OdsInstanceContext + { + ContextKey = contextKey, + ContextValue = contextValue, + OdsInstance = odsInstance1 + }; + + var contextKey2 = "contextKey2"; + var contextValue2 = "contextValue2"; + var newOdsInstanceContext2 = new OdsInstanceContext + { + ContextKey = contextKey2, + ContextValue = contextValue2, + OdsInstance = odsInstance1 + }; + Save(newOdsInstanceContext, newOdsInstanceContext2); + + var updateContextKey = "contextKey2"; + var updateContextValue = "contextValue2"; + var editOdsInstanceContext = new Mock(); + editOdsInstanceContext.Setup(x => x.OdsInstanceId).Returns(odsInstance1.OdsInstanceId); + editOdsInstanceContext.Setup(x => x.ContextKey).Returns(updateContextKey); + editOdsInstanceContext.Setup(x => x.ContextValue).Returns(updateContextValue); + editOdsInstanceContext.Setup(x => x.Id).Returns(newOdsInstanceContext.OdsInstanceContextId); + + Assert.Throws(() => + { + Transaction(usersContext => + { + var command = new EditOdsInstanceContextCommand(usersContext); + var updatedOdsInstanceContext = command.Execute(editOdsInstanceContext.Object); + updatedOdsInstanceContext.ShouldNotBeNull(); + updatedOdsInstanceContext.OdsInstanceContextId.ShouldBeGreaterThan(0); + updatedOdsInstanceContext.OdsInstanceContextId.ShouldBe(newOdsInstanceContext.OdsInstanceContextId); + updatedOdsInstanceContext.OdsInstance.OdsInstanceId.ShouldBe(odsInstance1.OdsInstanceId); + updatedOdsInstanceContext.ContextKey.ShouldBe(updateContextKey); + updatedOdsInstanceContext.ContextValue.ShouldBe(updateContextValue); + }); + }); + } + + [Test] + public void ShouldFailToEditWithInvalidData() + { + var odsInstance1 = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods1", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var odsInstance2 = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods2", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + Save(odsInstance2); + + var contextKey = "contextKey"; + var contextValue = "contextValue"; + var newOdsInstanceContext = new OdsInstanceContext + { + ContextKey = contextKey, + ContextValue = contextValue, + OdsInstance = odsInstance1 + }; + Save(newOdsInstanceContext); + + var updateContextKey = "contextKey2"; + var updateContextValue = string.Empty; + var editOdsInstanceContext = new Mock(); + editOdsInstanceContext.Setup(x => x.OdsInstanceId).Returns(odsInstance2.OdsInstanceId); + editOdsInstanceContext.Setup(x => x.ContextKey).Returns(updateContextKey); + editOdsInstanceContext.Setup(x => x.ContextValue).Returns(updateContextValue); + editOdsInstanceContext.Setup(x => x.Id).Returns(-1); + + Assert.Throws>(() => + { + Transaction(usersContext => + { + var command = new EditOdsInstanceContextCommand(usersContext); + var result = command.Execute(editOdsInstanceContext.Object); + }); + }); + } + +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditOdsInstanceDerivativeTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditOdsInstanceDerivativeTests.cs new file mode 100644 index 000000000..565c5989c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditOdsInstanceDerivativeTests.cs @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Moq; +using NUnit.Framework; +using Shouldly; +using Microsoft.EntityFrameworkCore; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class EditOdsInstanceDerivativeTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldEditOdsInstanceDerivative() + { + var odsInstance1 = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods1", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var odsInstance2 = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods2", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + Save(odsInstance2); + + var derivativeType = "ReadReplica"; + var connectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False"; + var newOdsInstanceDerivative = new OdsInstanceDerivative + { + ConnectionString = connectionString, + DerivativeType = derivativeType, + OdsInstance = odsInstance1 + }; + Save(newOdsInstanceDerivative); + + var updateDerivativeType = "ReadReplica"; + var updateConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods_2;Integrated Security=True;Encrypt=False"; + var editOdsInstanceDerivative = new Mock(); + editOdsInstanceDerivative.Setup(x => x.OdsInstanceId).Returns(odsInstance2.OdsInstanceId); + editOdsInstanceDerivative.Setup(x => x.DerivativeType).Returns(updateDerivativeType); + editOdsInstanceDerivative.Setup(x => x.ConnectionString).Returns(updateConnectionString); + editOdsInstanceDerivative.Setup(x => x.Id).Returns(newOdsInstanceDerivative.OdsInstanceDerivativeId); + + Transaction(usersContext => + { + var command = new EditOdsInstanceDerivativeCommand(usersContext); + var updatedOdsInstanceDerivative = command.Execute(editOdsInstanceDerivative.Object); + updatedOdsInstanceDerivative.ShouldNotBeNull(); + updatedOdsInstanceDerivative.OdsInstanceDerivativeId.ShouldBeGreaterThan(0); + updatedOdsInstanceDerivative.OdsInstanceDerivativeId.ShouldBe(newOdsInstanceDerivative.OdsInstanceDerivativeId); + updatedOdsInstanceDerivative.OdsInstance.OdsInstanceId.ShouldBe(odsInstance2.OdsInstanceId); + updatedOdsInstanceDerivative.DerivativeType.ShouldBe(updateDerivativeType); + updatedOdsInstanceDerivative.ConnectionString.ShouldBe(updateConnectionString); + }); + } + + [Test] + public void ShouldFailOdsInstanceDerivativeCombinedKey() + { + var odsInstance1 = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods1", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var derivativeType = "ReadReplica"; + var connectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False"; + var newOdsInstanceDerivative = new OdsInstanceDerivative + { + ConnectionString = connectionString, + DerivativeType = derivativeType, + OdsInstance = odsInstance1 + }; + + var newDerivativeType = "Snapshot"; + var newOdsInstanceDerivative2 = new OdsInstanceDerivative + { + ConnectionString = connectionString, + DerivativeType = newDerivativeType, + OdsInstance = odsInstance1 + }; + Save(newOdsInstanceDerivative, newOdsInstanceDerivative2); + + var updateDerivativeType = "Snapshot"; + var updateConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods_2;Integrated Security=True;Encrypt=False"; + var editOdsInstanceDerivative = new Mock(); + editOdsInstanceDerivative.Setup(x => x.OdsInstanceId).Returns(odsInstance1.OdsInstanceId); + editOdsInstanceDerivative.Setup(x => x.DerivativeType).Returns(updateDerivativeType); + editOdsInstanceDerivative.Setup(x => x.ConnectionString).Returns(updateConnectionString); + editOdsInstanceDerivative.Setup(x => x.Id).Returns(newOdsInstanceDerivative.OdsInstanceDerivativeId); + + Assert.Throws(() => + { + Transaction(usersContext => + { + var command = new EditOdsInstanceDerivativeCommand(usersContext); + var updatedOdsInstanceDerivative = command.Execute(editOdsInstanceDerivative.Object); + updatedOdsInstanceDerivative.ShouldNotBeNull(); + updatedOdsInstanceDerivative.OdsInstanceDerivativeId.ShouldBeGreaterThan(0); + updatedOdsInstanceDerivative.OdsInstanceDerivativeId.ShouldBe(newOdsInstanceDerivative.OdsInstanceDerivativeId); + updatedOdsInstanceDerivative.OdsInstance.OdsInstanceId.ShouldBe(odsInstance1.OdsInstanceId); + updatedOdsInstanceDerivative.DerivativeType.ShouldBe(updateDerivativeType); + updatedOdsInstanceDerivative.ConnectionString.ShouldBe(updateConnectionString); + }); + }); + } + + [Test] + public void ShouldFailToEditWithInvalidId() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods1", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + Save(odsInstance); + + var updateDerivativeType = "ReadReplica"; + var updateConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods_2;Integrated Security=True;Encrypt=False"; + var editOdsInstanceDerivative = new Mock(); + editOdsInstanceDerivative.Setup(x => x.OdsInstanceId).Returns(odsInstance.OdsInstanceId); + editOdsInstanceDerivative.Setup(x => x.DerivativeType).Returns(updateDerivativeType); + editOdsInstanceDerivative.Setup(x => x.ConnectionString).Returns(updateConnectionString); + editOdsInstanceDerivative.Setup(x => x.Id).Returns(-1); + + Assert.Throws>(() => + { + Transaction(usersContext => + { + var command = new EditOdsInstanceDerivativeCommand(usersContext); + var updatedOdsInstanceDerivative = command.Execute(editOdsInstanceDerivative.Object); + }); + }); + } + +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditProfileCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditProfileCommandTests.cs new file mode 100644 index 000000000..b9779a0d0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditProfileCommandTests.cs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Moq; +using NUnit.Framework; +using Shouldly; +using Profile = EdFi.Admin.DataAccess.Models.Profile; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class EditProfileCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldEditProfile() + { + var profileName = "Test-Profile"; + var definition = "" + + "" + + "" + + "" + + "" + + ""; + var newProfile = new Profile() + { + ProfileName = profileName, + ProfileDefinition = definition, + }; + + Save(newProfile); + + var updateProfileName = "Test-Profile-Update"; + var editProfile = new Mock(); + editProfile.Setup(x => x.Name).Returns(updateProfileName); + var updateDefinition = "" + + "" + + "" + + "" + + "" + + ""; + editProfile.Setup(x => x.Definition).Returns(updateDefinition); + editProfile.Setup(x => x.Id).Returns(newProfile.ProfileId); + + Transaction(usersContext => + { + var command = new EditProfileCommand(usersContext); + var updatedProfile = command.Execute(editProfile.Object); + updatedProfile.ShouldNotBeNull(); + updatedProfile.ProfileId.ShouldBeGreaterThan(0); + updatedProfile.ProfileId.ShouldBe(newProfile.ProfileId); + updatedProfile.ProfileName.ShouldBe(updateProfileName); + updatedProfile.ProfileDefinition.ShouldBe(updateDefinition); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditVendorCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditVendorCommandTests.cs index 88dd7aed9..ad5897fd1 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditVendorCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/EditVendorCommandTests.cs @@ -9,10 +9,10 @@ using NUnit.Framework; using Shouldly; using System.Linq; -using EdFi.Admin.DataAccess.Models; +using EdFi.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; using VendorUser = EdFi.Admin.DataAccess.Models.User; using EdFi.Ods.AdminApi.Infrastructure.Helpers; -using Microsoft.EntityFrameworkCore; namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; @@ -54,7 +54,7 @@ public void Init() public void ShouldEditVendorWithContact() { var newVendorData = new Mock(); - newVendorData.Setup(v => v.VendorId).Returns(_vendorId); + newVendorData.Setup(v => v.Id).Returns(_vendorId); newVendorData.Setup(v => v.Company).Returns("new vendor name"); newVendorData.Setup(v => v.NamespacePrefixes).Returns("new namespace prefix"); newVendorData.Setup(v => v.ContactName).Returns("new contact name"); @@ -68,9 +68,9 @@ public void ShouldEditVendorWithContact() Transaction(usersContext => { - var changedVendor = usersContext.Vendors - .Include(x => x.VendorNamespacePrefixes) - .Include(x => x.Users) + var changedVendor = usersContext.Vendors + .Include(v => v.Users) + .Include(v => v.VendorNamespacePrefixes) .Single(v => v.VendorId == _vendorId); changedVendor.VendorName.ShouldBe("new vendor name"); changedVendor.VendorNamespacePrefixes.First().NamespacePrefix.ShouldBe("new namespace prefix"); @@ -83,7 +83,7 @@ public void ShouldEditVendorWithContact() public void ShouldEditVendorWithNoNameSpacePrefix() { var newVendorData = new Mock(); - newVendorData.Setup(v => v.VendorId).Returns(_vendorId); + newVendorData.Setup(v => v.Id).Returns(_vendorId); newVendorData.Setup(v => v.Company).Returns("new vendor name"); newVendorData.Setup(v => v.NamespacePrefixes).Returns(string.Empty); newVendorData.Setup(v => v.ContactName).Returns("new contact name"); @@ -97,9 +97,9 @@ public void ShouldEditVendorWithNoNameSpacePrefix() Transaction(usersContext => { - var changedVendor = usersContext.Vendors - .Include(x => x.VendorNamespacePrefixes) - .Include(x => x.Users).Single(v => v.VendorId == _vendorId); + var changedVendor = usersContext.Vendors + .Include(v => v.Users) + .Include(v => v.VendorNamespacePrefixes).Single(v => v.VendorId == _vendorId); changedVendor.VendorName.ShouldBe("new vendor name"); changedVendor.VendorNamespacePrefixes.ShouldBeEmpty(); changedVendor.Users.First().FullName.ShouldBe("new contact name"); @@ -111,7 +111,7 @@ public void ShouldEditVendorWithNoNameSpacePrefix() public void ShouldEditVendorByAddingNewNameSpacePrefix() { var newVendorData = new Mock(); - newVendorData.Setup(v => v.VendorId).Returns(_vendorWithNoNameSpaceId); + newVendorData.Setup(v => v.Id).Returns(_vendorWithNoNameSpaceId); newVendorData.Setup(v => v.Company).Returns("new vendor name"); newVendorData.Setup(v => v.NamespacePrefixes).Returns("new namespace prefix"); newVendorData.Setup(v => v.ContactName).Returns("new contact name"); @@ -125,10 +125,9 @@ public void ShouldEditVendorByAddingNewNameSpacePrefix() Transaction(usersContext => { - var changedVendor = usersContext.Vendors - .Include(x => x.VendorNamespacePrefixes) - .Include(x => x.Users). - Single(v => v.VendorId == _vendorWithNoNameSpaceId); + var changedVendor = usersContext.Vendors + .Include(v => v.Users) + .Include(v => v.VendorNamespacePrefixes).Single(v => v.VendorId == _vendorWithNoNameSpaceId); changedVendor.VendorName.ShouldBe("new vendor name"); changedVendor.VendorNamespacePrefixes.First().NamespacePrefix.ShouldBe("new namespace prefix"); changedVendor.Users.First().FullName.ShouldBe("new contact name"); @@ -142,8 +141,8 @@ public void ShouldEditVendorByAddingMultipleNameSpacePrefixes() var newVendorData = new Mock(); Transaction(usersContext => { - var originalVendor = usersContext.Vendors - .Include(x => x.VendorNamespacePrefixes).Single(v => v.VendorId == _vendorId); + var originalVendor = usersContext.Vendors + .Include(v => v.VendorNamespacePrefixes).Single(v => v.VendorId == _vendorId); originalVendor.VendorNamespacePrefixes.Single().NamespacePrefix.ShouldBe(OriginalVendorNamespacePrefix); }); var newNamespacePrefixes = new List @@ -153,7 +152,7 @@ public void ShouldEditVendorByAddingMultipleNameSpacePrefixes() "http://www.test2.com/", "http://www.test3.com/" }; - newVendorData.Setup(v => v.VendorId).Returns(_vendorId); + newVendorData.Setup(v => v.Id).Returns(_vendorId); newVendorData.Setup(v => v.Company).Returns("new vendor name"); newVendorData.Setup(v => v.NamespacePrefixes).Returns(newNamespacePrefixes.ToDelimiterSeparated()); newVendorData.Setup(v => v.ContactName).Returns("new contact name"); @@ -167,10 +166,9 @@ public void ShouldEditVendorByAddingMultipleNameSpacePrefixes() Transaction(usersContext => { - var changedVendor = usersContext.Vendors - .Include(x => x.VendorNamespacePrefixes) - .Include(x => x.Users) - .Single(v => v.VendorId == _vendorId); + var changedVendor = usersContext.Vendors + .Include(v => v.Users) + .Include(v => v.VendorNamespacePrefixes).Single(v => v.VendorId == _vendorId); changedVendor.VendorName.ShouldBe("new vendor name"); changedVendor.VendorNamespacePrefixes.Select(x => x.NamespacePrefix).ShouldBe(newNamespacePrefixes); changedVendor.Users.First().FullName.ShouldBe("new contact name"); @@ -187,13 +185,12 @@ public void ShouldNotAddEmptyNameSpacePrefixesWhileEditingVendor(string inputNam var newVendorData = new Mock(); Transaction(usersContext => { - var originalVendor = usersContext.Vendors - .Include(x => x.VendorNamespacePrefixes) - .Single(v => v.VendorId == _vendorId); + var originalVendor = usersContext.Vendors + .Include(v => v.VendorNamespacePrefixes).Single(v => v.VendorId == _vendorId); originalVendor.VendorNamespacePrefixes.Single().NamespacePrefix.ShouldBe(OriginalVendorNamespacePrefix); }); - newVendorData.Setup(v => v.VendorId).Returns(_vendorId); + newVendorData.Setup(v => v.Id).Returns(_vendorId); newVendorData.Setup(v => v.Company).Returns("new vendor name"); newVendorData.Setup(v => v.NamespacePrefixes).Returns(inputNamespacePrefixes); newVendorData.Setup(v => v.ContactName).Returns("new contact name"); @@ -207,10 +204,9 @@ public void ShouldNotAddEmptyNameSpacePrefixesWhileEditingVendor(string inputNam Transaction(usersContext => { - var changedVendor = usersContext.Vendors - .Include(x => x.Users) - .Include(x => x.VendorNamespacePrefixes) - .Single(v => v.VendorId == _vendorId); + var changedVendor = usersContext.Vendors + .Include(v => v.Users) + .Include(v => v.VendorNamespacePrefixes).Single(v => v.VendorId == _vendorId); changedVendor.VendorName.ShouldBe("new vendor name"); changedVendor.VendorNamespacePrefixes.Select(x => x.NamespacePrefix).ToDelimiterSeparated().ShouldBe(expectedNamespacePrefixes); changedVendor.Users.First().FullName.ShouldBe("new contact name"); @@ -225,13 +221,12 @@ public void ShouldEditVendorByRemovingNameSpacePrefix() Transaction(usersContext => { - var originalVendor = usersContext.Vendors - .Include (x => x.VendorNamespacePrefixes) - .Single(v => v.VendorId == _vendorId); + var originalVendor = usersContext.Vendors + .Include(v => v.VendorNamespacePrefixes).Single(v => v.VendorId == _vendorId); originalVendor.VendorNamespacePrefixes.Single().NamespacePrefix.ShouldBe(OriginalVendorNamespacePrefix); }); - newVendorData.Setup(v => v.VendorId).Returns(_vendorId); + newVendorData.Setup(v => v.Id).Returns(_vendorId); newVendorData.Setup(v => v.Company).Returns("new vendor name"); newVendorData.Setup(v => v.NamespacePrefixes).Returns(""); newVendorData.Setup(v => v.ContactName).Returns("new contact name"); @@ -245,10 +240,9 @@ public void ShouldEditVendorByRemovingNameSpacePrefix() Transaction(usersContext => { - var changedVendor = usersContext.Vendors - .Include(x => x.VendorNamespacePrefixes) - .Include(x => x.Users) - .Single(v => v.VendorId == _vendorId); + var changedVendor = usersContext.Vendors + .Include(v => v.Users) + .Include(v => v.VendorNamespacePrefixes).Single(v => v.VendorId == _vendorId); changedVendor.VendorName.ShouldBe("new vendor name"); changedVendor.VendorNamespacePrefixes.ShouldBeEmpty(); changedVendor.Users.First().FullName.ShouldBe("new contact name"); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/RegenerateApiClientSecretCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/RegenerateApiClientSecretCommandTests.cs index 9e69851b8..8313b857b 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/RegenerateApiClientSecretCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/RegenerateApiClientSecretCommandTests.cs @@ -9,10 +9,11 @@ using EdFi.Admin.DataAccess.Models; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; using NUnit.Framework; using Shouldly; using VendorUser = EdFi.Admin.DataAccess.Models.User; +using EdFi.Ods.AdminApi.Common.Infrastructure; namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; @@ -20,7 +21,7 @@ namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; public class RegenerateApiClientSecretCommandTests : PlatformUsersContextTestBase { [Test] - public void ShouldFailIfApplicationDoesNotExist() + public void ShouldFailIfApiClientDoesNotExist() { Transaction(usersContext => { @@ -29,24 +30,6 @@ public void ShouldFailIfApplicationDoesNotExist() }); } - [Test] - public void ShouldReportFailureIfApiClientDoesNotExist() - { - var application = new Application - { - ApplicationName = "Api Client Secret Test App", - OperationalContextUri = OperationalContext.DefaultOperationalContextUri - }; - - Save(application); - - Transaction(usersContext => - { - var command = new RegenerateApiClientSecretCommand(usersContext); - Assert.Throws(() => command.Execute(application.ApplicationId)); - }); - } - [Test] public void ShouldUpdateApiClientSecret() { @@ -94,7 +77,7 @@ public void ShouldUpdateApiClientSecret() //Simulate the automatic hashing performed by using the key/secret on the API. Transaction(usersContext => { - var odsSideApiClient = usersContext.Clients.Single(c => c.ApiClientId == apiClient.ApiClientId); + var odsSideApiClient = usersContext.ApiClients.Single(c => c.ApiClientId == apiClient.ApiClientId); odsSideApiClient.Secret = "SIMULATED HASH OF " + originalSecret; odsSideApiClient.SecretIsHashed = true; }); @@ -103,12 +86,13 @@ public void ShouldUpdateApiClientSecret() Transaction(usersContext => { var command = new RegenerateApiClientSecretCommand(usersContext); - result = command.Execute(application.ApplicationId); + result = command.Execute(apiClient.ApiClientId); }); - var updatedApiClient = Transaction(usersContext => usersContext.Clients.Single(c => c.ApiClientId == apiClient.ApiClientId)); + var updatedApiClient = Transaction(usersContext => usersContext.ApiClients.Single(c => c.ApiClientId == apiClient.ApiClientId)); result.Key.ShouldBe(orignalKey); + result.Name.ShouldBe("Integration Test"); result.Secret.ShouldNotBe(originalSecret); result.Secret.ShouldNotBe("SIMULATED HASH OF " + originalSecret); result.Secret.ShouldNotBeEmpty(); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/RegenerateApplicationApiClientSecretCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/RegenerateApplicationApiClientSecretCommandTests.cs new file mode 100644 index 000000000..2ce07e04e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/RegenerateApplicationApiClientSecretCommandTests.cs @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using NUnit.Framework; +using Shouldly; +using VendorUser = EdFi.Admin.DataAccess.Models.User; +using EdFi.Ods.AdminApi.Common.Infrastructure; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class RegenerateApplicationApiClientSecretCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldFailIfApplicationDoesNotExist() + { + Transaction(usersContext => + { + var command = new RegenerateApplicationApiClientSecretCommand(usersContext); + Assert.Throws>(() => command.Execute(0)); + }); + } + + [Test] + public void ShouldReportFailureIfApiClientDoesNotExist() + { + var application = new Application + { + ApplicationName = "Api Client Secret Test App", + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + + Save(application); + + Transaction(usersContext => + { + var command = new RegenerateApplicationApiClientSecretCommand(usersContext); + Assert.Throws(() => command.Execute(application.ApplicationId)); + }); + } + + [Test] + public void ShouldUpdateApiClientSecret() + { + var vendor = new Vendor + { + VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://tests.com" } }, + VendorName = "Integration Tests" + }; + + var user = new VendorUser + { + Email = "nobody@nowhere.com", + FullName = "Integration Tests", + Vendor = vendor + }; + + var profile = new Profile + { + ProfileName = "Test Profile" + }; + + var apiClient = new ApiClient(true) + { + Name = "Integration Test" + }; + + var application = new Application + { + ApplicationName = "Test Application", + ClaimSetName = "FakeClaimSet", + ApiClients = new List(), + Vendor = vendor, + Profiles = new List(), + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + + application.ApiClients.Add(apiClient); + application.Profiles.Add(profile); + + Save(vendor, user, profile, application); + + var orignalKey = apiClient.Key; + var originalSecret = apiClient.Secret; + + //Simulate the automatic hashing performed by using the key/secret on the API. + Transaction(usersContext => + { + var odsSideApiClient = usersContext.ApiClients.Single(c => c.ApiClientId == apiClient.ApiClientId); + odsSideApiClient.Secret = "SIMULATED HASH OF " + originalSecret; + odsSideApiClient.SecretIsHashed = true; + }); + + RegenerateApplicationApiClientSecretResult result = null; + Transaction(usersContext => + { + var command = new RegenerateApplicationApiClientSecretCommand(usersContext); + result = command.Execute(application.ApplicationId); + }); + + var updatedApiClient = Transaction(usersContext => usersContext.ApiClients.Single(c => c.ApiClientId == apiClient.ApiClientId)); + + result.Key.ShouldBe(orignalKey); + result.Secret.ShouldNotBe(originalSecret); + result.Secret.ShouldNotBe("SIMULATED HASH OF " + originalSecret); + result.Secret.ShouldNotBeEmpty(); + + updatedApiClient.Key.ShouldBe(result.Key); + updatedApiClient.Secret.ShouldBe(result.Secret); + updatedApiClient.SecretIsHashed.ShouldBe(false); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetAllActionsQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetAllActionsQueryTests.cs new file mode 100644 index 000000000..a88da87f0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetAllActionsQueryTests.cs @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetAllActionsQueryTests : SecurityDataTestBase +{ + [Test] + public void ShouldGetAllActions() + { + LoadSeedData(); + using var securityContext = TestContext; + var query = new GetAllActionsQuery(securityContext, Testing.GetAppSettings()); + var resultNames = query.Execute().Select(x => x.ActionName).ToList(); + + resultNames.Count.ShouldBe(4); + + resultNames.ShouldContain("Create"); + resultNames.ShouldContain("Read"); + } + + [Test] + public void ShouldGetAllActions_With_Offset_and_Limit_and_order_by_id_Asc() + { + LoadSeedData(); + var offset = 1; + var limit = 2; + + using var securityContext = TestContext; + var query = new GetAllActionsQuery(securityContext, Testing.GetAppSettings()); + var resultNames = query.Execute( + new CommonQueryParams(offset, limit, "id", "Ascending"), + null, null).Select(x => x.ActionName).ToList(); + + resultNames.Count.ShouldBe(2); + + resultNames.ShouldContain("Create"); + resultNames.ShouldContain("Update"); + } + + [Test] + public void ShouldGetAllActions_With_Offset_and_Limit_and_order_by_Name_Asc() + { + LoadSeedData(); + var offset = 1; + var limit = 2; + + using var securityContext = TestContext; + var query = new GetAllActionsQuery(securityContext, Testing.GetAppSettings()); + var resultNames = query.Execute( + new CommonQueryParams(offset, limit, "Name", "Ascending"), + null, null).Select(x => x.ActionName).ToList(); + + resultNames.Count.ShouldBe(2); + + resultNames.ShouldContain("Delete"); + resultNames.ShouldContain("Read"); + } + + [Test] + public void ShouldGetAllActions_With_Offset_and_Limit_and_order_by_Name_Desc() + { + LoadSeedData(); + var offset = 1; + var limit = 2; + + using var securityContext = TestContext; + var query = new GetAllActionsQuery(securityContext, Testing.GetAppSettings()); + var resultNames = query.Execute( + new CommonQueryParams(offset, limit, "Name", "Descending"), + null, null).Select(x => x.ActionName).ToList(); + + resultNames.Count.ShouldBe(2); + + resultNames.ShouldContain("Read"); + resultNames.ShouldContain("Delete"); + } + + [Test] + public void ShouldGetAllActions_With_Name() + { + LoadSeedData(); + var name = "Delete"; + using var securityContext = TestContext; + var query = new GetAllActionsQuery(securityContext, Testing.GetAppSettings()); + var resultNames = query.Execute( + new CommonQueryParams(), + null, name).Select(x => x.ActionName).ToList(); + + resultNames.Count.ShouldBe(1); + + resultNames.ShouldContain("Delete"); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetAllApplicationsQuery.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetAllApplicationsQuery.cs new file mode 100644 index 000000000..1fe2be072 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetAllApplicationsQuery.cs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; +using VendorUser = EdFi.Admin.DataAccess.Models.User; + + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetAllApplicationsQueryTests : PlatformUsersContextTestBase +{ + private IOptions _options { get; set; } + + [SetUp] + public virtual async Task FixtureSetup() + { + _options = Testing.GetAppSettings(); + _options.Value.PreventDuplicateApplications = false; + LoadApplications(3); + await Task.Yield(); + } + + [Test] + public void ShouldGetAllApplications() + { + var result = new List(); + + Transaction(usersContext => + { + var query = new GetAllApplicationsQuery(usersContext, _options); + result = query.Execute( + new CommonQueryParams(), null, null, null, null).ToList(); + }); + + result.Count.ShouldBeGreaterThan(0); + } + + [Test] + public void ShouldGetAllApplications_With_Offset_and_Limit() + { + var offset = 1; + var limit = 2; + Transaction(usersContext => + { + var query = new GetAllApplicationsQuery(usersContext, _options); + var result = query.Execute( + new CommonQueryParams(offset, limit), null, null, null, null); + + result.Count.ShouldBeGreaterThan(0); + result.Count.ShouldBe(2); + }); + } + + private void LoadApplications(int count = 1) + { + for (int i = 0; i < count; i++) + { + var vendor = new Vendor { VendorName = $"test vendor {Guid.NewGuid().ToString()}" }; + var odsInstance = new OdsInstance + { + Name = $"Test Instance {Guid.NewGuid().ToString()}", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var application = new Application + { + ApplicationName = $"test app {Guid.NewGuid().ToString()}", + ClaimSetName = "test claim set", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + }; + var user = new VendorUser + { + Email = "", + FullName = application.ApplicationName, + Vendor = vendor + }; + + var apiClient = new ApiClient + { + Application = application, + Key = $"key {Guid.NewGuid().ToString()}", + Secret = "secret", + Name = application.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active", + User = user, + }; + + var apiClientOdsIntance = new ApiClientOdsInstance + { + ApiClient = apiClient, + OdsInstance = odsInstance, + }; + + Save(odsInstance, vendor, application, apiClient, apiClientOdsIntance); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetAllClaimSetsQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetAllClaimSetsQueryTests.cs new file mode 100644 index 000000000..83680b0d0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetAllClaimSetsQueryTests.cs @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Security.DataAccess.Models; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetAllClaimSetsQueryTests : SecurityDataTestBase +{ + public GetAllClaimSetsQueryTests() + { + SeedSecurityContextOnFixtureSetup = true; + } + + [Test] + public void Should_Retreive_ClaimSetNames() + { + var claimSet1 = GetClaimSet(); + var claimSet2 = GetClaimSet(); + Save(claimSet1, claimSet2); + + var claimSetNames = Transaction(securityContext => + { + var query = new GetAllClaimSetsQuery(securityContext, Testing.GetAppSettings()); + return query.Execute().Select(x => x.Name).ToArray(); + }); + + claimSetNames.ShouldContain(claimSet1.ClaimSetName); + claimSetNames.ShouldContain(claimSet2.ClaimSetName); + } + + [Test] + public void Should_Retreive_ClaimSetNames_With_Offset_And_Limit() + { + var claimSet1 = GetClaimSet(); + var claimSet2 = GetClaimSet(); + Save(claimSet1, claimSet2); + + var claimSetNames = Transaction(securityContext => + { + var query = new GetAllClaimSetsQuery(securityContext, Testing.GetAppSettings()); + return query.Execute( + new CommonQueryParams(), + null, + null).Select(x => x.Name).ToArray(); + }); + + claimSetNames.ShouldContain(claimSet1.ClaimSetName); + claimSetNames.ShouldContain(claimSet2.ClaimSetName); + } + + [Test] + public void Should_Retreive_ClaimSetNames_With_Id() + { + var claimSet1 = GetClaimSet(); + var claimSet2 = GetClaimSet(); + Save(claimSet1, claimSet2); + + var claimSetNames = Transaction(securityContext => + { + var query = new GetAllClaimSetsQuery(securityContext, Testing.GetAppSettings()); + return query.Execute(new CommonQueryParams(), + claimSet2.ClaimSetId, + null).Select(x => x.Name).ToArray(); + }); + + claimSetNames.Length.ShouldBe(1); + claimSetNames.ShouldContain(claimSet2.ClaimSetName); + } + + [Test] + public void Should_Retreive_ClaimSetNames_With_Name() + { + var claimSet1 = GetClaimSet(); + var claimSet2 = GetClaimSet(); + Save(claimSet1, claimSet2); + + var claimSetNames = Transaction(securityContext => + { + var query = new GetAllClaimSetsQuery(securityContext, Testing.GetAppSettings()); + return query.Execute(new CommonQueryParams(), + null, + claimSet2.ClaimSetName).Select(x => x.Name).ToArray(); + }); + + claimSetNames.Length.ShouldBe(1); + claimSetNames.ShouldContain(claimSet2.ClaimSetName); + } + + private static int _claimSetId = 0; + private static ClaimSet GetClaimSet() + { + return new ClaimSet + { + ClaimSetName = $"Test Claim Set {_claimSetId++} - {DateTime.Now:O}" + }; + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApiClientByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApiClientByIdQueryTests.cs new file mode 100644 index 000000000..9b12c8730 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApiClientByIdQueryTests.cs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; +using VendorUser = EdFi.Admin.DataAccess.Models.User; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +internal class GetApiClientByIdQueryTests : PlatformUsersContextTestBase +{ + private IOptions _options { get; set; } + private int secondApiClientId = 0; + + [SetUp] + public virtual async Task FixtureSetup() + { + _options = Testing.GetAppSettings(); + _options.Value.PreventDuplicateApplications = false; + LoadApiClients(); + await Task.Yield(); + } + + [Test] + public void ShouldGetApliClientById() + { + ApiClient result = null; + + Transaction(usersContext => + { + var query = new GetApiClientByIdQuery(usersContext); + result = query.Execute(secondApiClientId); + }); + + result.Equals(null).ShouldBeFalse(); + } + + private void LoadApiClients() + { + var vendor = new Vendor { VendorName = $"test vendor {Guid.NewGuid().ToString()}" }; + var odsInstance = new OdsInstance + { + Name = $"Test Instance {Guid.NewGuid().ToString()}", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var application = new Application + { + ApplicationName = $"test app {Guid.NewGuid().ToString()}", + ClaimSetName = "test claim set", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + }; + var user = new VendorUser + { + Email = "", + FullName = application.ApplicationName, + Vendor = vendor + }; + + var apiClient1 = new ApiClient + { + Application = application, + Key = $"key {Guid.NewGuid().ToString()}", + Secret = "secret", + Name = application.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active", + User = user, + }; + + var apiClient2 = new ApiClient + { + Application = application, + Key = $"key {Guid.NewGuid().ToString()}", + Secret = "secret", + Name = $"{application.ApplicationName} 2", + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active", + User = user, + }; + + var apiClientOdsIntance1 = new ApiClientOdsInstance + { + ApiClient = apiClient1, + OdsInstance = odsInstance, + }; + + var apiClientOdsIntance2 = new ApiClientOdsInstance + { + ApiClient = apiClient2, + OdsInstance = odsInstance, + }; + + Save(odsInstance, vendor, application, apiClient1, apiClient2, apiClientOdsIntance1, apiClientOdsIntance2); + + secondApiClientId = apiClient2.ApiClientId; + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApiClientIdByApplicationIdQueryTest.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApiClientIdByApplicationIdQueryTest.cs new file mode 100644 index 000000000..5195e9df6 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApiClientIdByApplicationIdQueryTest.cs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; +using System.Collections.Generic; +using System.Linq; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using System; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetApiClientIdByApplicationIdQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldGetApiClientIdByApplicationIdData() + { + var vendor = new Vendor { VendorName = "test vendor" }; + var odsInstance = new OdsInstance + { + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var application = new Application + { + ApplicationName = "test application", + ClaimSetName = "test claim set", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + }; + + var apiClient = new ApiClient + { + Application = application, + Key = "key", + Secret = "secret", + Name = application.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active" + }; + Save(odsInstance, vendor, application, apiClient); + int applicationId = application.ApplicationId; + Transaction(usersContext => + { + var getApiClientIdByApplicationIdQuery = new GetApiClientIdByApplicationIdQuery(usersContext); + var results = getApiClientIdByApplicationIdQuery.Execute(applicationId); + results.ShouldNotBeNull(); + }); + } + + [Test] + public void ShouldNotGetApiClientIdWithDifferentApplicationId() + { + var vendor = new Vendor { VendorName = "test vendor" }; + var odsInstance = new OdsInstance + { + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var application = new Application + { + ApplicationName = "test application", + ClaimSetName = "test claim set", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + }; + + var apiClient = new ApiClient + { + Application = application, + Key = "key", + Secret = "secret", + Name = application.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active" + }; + Save(odsInstance, vendor, application, apiClient); + int applicationId = 999; + Transaction(usersContext => + { + var getApiClientIdByApplicationIdQuery = new GetApiClientIdByApplicationIdQuery(usersContext); + Should.Throw>(() => + { + getApiClientIdByApplicationIdQuery.Execute(applicationId); + }); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApiClientOdsInstanceQueryTest.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApiClientOdsInstanceQueryTest.cs new file mode 100644 index 000000000..66c07fd7d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApiClientOdsInstanceQueryTest.cs @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; +using System.Collections.Generic; +using System.Linq; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using System; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetApiClientOdsInstanceQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldGetApiClientOdsInstanceData() + { + var odsInstance = new OdsInstance + { + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + var application = new Application + { + ApplicationName = "test application", + ClaimSetName = "test claim set", + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + }; + var apiClient = new ApiClient + { + Application = application, + Key = "key", + Secret = "secret", + Name = application.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active" + }; + + var apiClientOdsIntance = new ApiClientOdsInstance + { + ApiClient = apiClient, + OdsInstance = odsInstance, + }; + Save(apiClientOdsIntance); + int odsInstanceId = odsInstance.OdsInstanceId; + int apiClientId = apiClient.ApiClientId; + Transaction(usersContext => + { + var getApiClientOdsInstanceQuery = new GetApiClientOdsInstanceQuery(usersContext); + var results = getApiClientOdsInstanceQuery.Execute(apiClientId, odsInstanceId); + results.ShouldNotBeNull(); + }); + } + + [Test] + public void ShouldNotGetApiClientOdsInstanceWithDifferentApiClient() + { + var odsInstance = new OdsInstance + { + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + var application = new Application + { + ApplicationName = "test application", + ClaimSetName = "test claim set", + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + }; + var apiClient = new ApiClient + { + Application = application, + Key = "key", + Secret = "secret", + Name = application.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active" + }; + + var apiClientOdsIntance = new ApiClientOdsInstance + { + ApiClient = apiClient, + OdsInstance = odsInstance, + }; + Save(apiClientOdsIntance); + int odsInstanceId = odsInstance.OdsInstanceId; + int apiClientId = 999; + Transaction(usersContext => + { + var getApiClientOdsInstanceQuery = new GetApiClientOdsInstanceQuery(usersContext); + var results = getApiClientOdsInstanceQuery.Execute(apiClientId, odsInstanceId); + results.ShouldBeNull(); + }); + } + + [Test] + public void ShouldNotGetApiClientOdsInstanceWithDifferentOdsInstance() + { + var odsInstance = new OdsInstance + { + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + var application = new Application + { + ApplicationName = "test application", + ClaimSetName = "test claim set", + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + }; + var apiClient = new ApiClient + { + Application = application, + Key = "key", + Secret = "secret", + Name = application.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active" + }; + + var apiClientOdsIntance = new ApiClientOdsInstance + { + ApiClient = apiClient, + OdsInstance = odsInstance, + }; + Save(apiClientOdsIntance); + int odsInstanceId = 999; + int apiClientId = apiClient.ApiClientId; + Transaction(usersContext => + { + var getApiClientOdsInstanceQuery = new GetApiClientOdsInstanceQuery(usersContext); + var results = getApiClientOdsInstanceQuery.Execute(apiClientId, odsInstanceId); + results.ShouldBeNull(); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApiClientsByApplicationIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApiClientsByApplicationIdQueryTests.cs new file mode 100644 index 000000000..921e1615d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApiClientsByApplicationIdQueryTests.cs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; +using VendorUser = EdFi.Admin.DataAccess.Models.User; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetApiClientsByApplicationIdQueryTests : PlatformUsersContextTestBase +{ + private IOptions _options { get; set; } + private int applicationId = 0; + + [SetUp] + public virtual async Task FixtureSetup() + { + _options = Testing.GetAppSettings(); + _options.Value.PreventDuplicateApplications = false; + LoadApiClients(); + await Task.Yield(); + } + + [Test] + public void ShouldGetAllApliClientsPerApplication() + { + IReadOnlyList result = null; + + Transaction(usersContext => + { + var query = new GetApiClientsByApplicationIdQuery(usersContext); + result = query.Execute(applicationId); + }); + + result.Count.ShouldBe(2); + } + + private void LoadApiClients() + { + var vendor = new Vendor { VendorName = $"test vendor {Guid.NewGuid().ToString()}" }; + var odsInstance = new OdsInstance + { + Name = $"Test Instance {Guid.NewGuid().ToString()}", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var application = new Application + { + ApplicationName = $"test app {Guid.NewGuid().ToString()}", + ClaimSetName = "test claim set", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + }; + var user = new VendorUser + { + Email = "", + FullName = application.ApplicationName, + Vendor = vendor + }; + + var apiClient1 = new ApiClient + { + Application = application, + Key = $"key {Guid.NewGuid().ToString()}", + Secret = "secret", + Name = application.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active", + User = user, + }; + + var apiClient2 = new ApiClient + { + Application = application, + Key = $"key {Guid.NewGuid().ToString()}", + Secret = "secret", + Name = $"{application.ApplicationName} 2", + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active", + User = user, + }; + + var apiClientOdsIntance1 = new ApiClientOdsInstance + { + ApiClient = apiClient1, + OdsInstance = odsInstance, + }; + + var apiClientOdsIntance2 = new ApiClientOdsInstance + { + ApiClient = apiClient2, + OdsInstance = odsInstance, + }; + + Save(odsInstance, vendor, application, apiClient1, apiClient2, apiClientOdsIntance1, apiClientOdsIntance2); + + applicationId = application.ApplicationId; + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationByNameAndClaimsetQueryTest.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationByNameAndClaimsetQueryTest.cs new file mode 100644 index 000000000..2a40aa0fe --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationByNameAndClaimsetQueryTest.cs @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; +using System.Collections.Generic; +using System.Linq; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetApplicationByNameAndClaimsetQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldGetBasicApplicationData() + { + var vendor = new Vendor { VendorName = "test vendor" }; + var applicationName = "test application"; + var claimsetName = "test claim set"; + var application = new Application + { + ApplicationName = "test application", + ClaimSetName = "test claim set", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + vendor.Applications.Add(application); + Save(vendor); + + Transaction(usersContext => + { + var getApplicationByNameAndClaimsetQuery = new GetApplicationByNameAndClaimsetQuery(usersContext); + var results = getApplicationByNameAndClaimsetQuery.Execute(applicationName, claimsetName); + results.ApplicationName.ShouldBe("test application"); + results.ClaimSetName.ShouldBe("test claim set"); + }); + } + + [Test] + public void ShouldNotGetApplicationDataWithDifferentClaimset() + { + var vendor = new Vendor { VendorName = "test vendor" }; + var applicationName = "test application"; + var claimsetName = "test different claim set"; + var application = new Application + { + ApplicationName = "test application", + ClaimSetName = "test claim set", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + vendor.Applications.Add(application); + Save(vendor); + + Transaction(usersContext => + { + var getApplicationByNameAndClaimsetQuery = new GetApplicationByNameAndClaimsetQuery(usersContext); + var results = getApplicationByNameAndClaimsetQuery.Execute(applicationName, claimsetName); + results.ShouldBeNull(); + }); + } + + [Test] + public void ShouldNotGetApplicationDataWithDifferentApplicationName() + { + var vendor = new Vendor { VendorName = "test vendor" }; + var applicationName = "test different application"; + var claimsetName = "test claim set"; + var application = new Application + { + ApplicationName = "test application", + ClaimSetName = "test claim set", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + vendor.Applications.Add(application); + Save(vendor); + + Transaction(usersContext => + { + var getApplicationByNameAndClaimsetQuery = new GetApplicationByNameAndClaimsetQuery(usersContext); + var results = getApplicationByNameAndClaimsetQuery.Execute(applicationName, claimsetName); + results.ShouldBeNull(); + }); + } + + [Test] + public void ShouldNotGetApplicationDataWithDifferentApplicationNameAndClaimsetName() + { + var vendor = new Vendor { VendorName = "test vendor" }; + var applicationName = "test different application"; + var claimsetName = "test different claim set"; + var application = new Application + { + ApplicationName = "test application", + ClaimSetName = "test claim set", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + vendor.Applications.Add(application); + Save(vendor); + + Transaction(usersContext => + { + var getApplicationByNameAndClaimsetQuery = new GetApplicationByNameAndClaimsetQuery(usersContext); + var results = getApplicationByNameAndClaimsetQuery.Execute(applicationName, claimsetName); + results.ShouldBeNull(); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationByOdsInstanceIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationByOdsInstanceIdQueryTests.cs new file mode 100644 index 000000000..bc1212d56 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationByOdsInstanceIdQueryTests.cs @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; +using VendorUser = EdFi.Admin.DataAccess.Models.User; +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetApplicationByOdsInstanceIdQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldGetBasicApplicationData() + { + var vendor = new Vendor { VendorName = "test vendor" }; + var odsInstance = new OdsInstance + { + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var application = new Application + { + ApplicationName = "test application", + ClaimSetName = "test claim set", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + }; + var user = new VendorUser + { + Email = "", + FullName = application.ApplicationName, + Vendor = vendor + }; + + var apiClient = new ApiClient + { + Application = application, + Key = "key", + Secret = "secret", + Name = application.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active", + User = user, + }; + + var apiClientOdsIntance = new ApiClientOdsInstance + { + ApiClient = apiClient, + OdsInstance = odsInstance, + }; + + Save(odsInstance, vendor, application, apiClient, apiClientOdsIntance); + + Transaction(usersContext => + { + var getApplicationsByOdsInstanceIdQuery = new GetApplicationsByOdsInstanceIdQuery(usersContext); + var results = getApplicationsByOdsInstanceIdQuery.Execute(odsInstance.OdsInstanceId); + results.Single().ApplicationName.ShouldBe("test application"); + results.Single().ClaimSetName.ShouldBe("test claim set"); + }); + } + + [Test] + public void ShouldGetApplicationEducationOrganization() + { + var vendor = new Vendor { VendorName = "test vendor" }; + var odsInstance = new OdsInstance + { + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + var application = new Application + { + ApplicationName = "test application", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + }; + + var applicationOrganization = new ApplicationEducationOrganization { Application = application }; + + application.ApplicationEducationOrganizations.Add(applicationOrganization); + + var user = new VendorUser + { + Email = "", + FullName = application.ApplicationName, + Vendor = vendor + }; + + var apiClient = new ApiClient + { + Application = application, + Key = "key", + Secret = "secret", + Name = application.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active", + User = user, + }; + + application.ApiClients.Add(apiClient); + + var apiClientOdsIntance = new ApiClientOdsInstance + { + ApiClient = apiClient, + OdsInstance = odsInstance, + }; + Save(odsInstance, vendor, application, apiClientOdsIntance); + + var organizationId = applicationOrganization.ApplicationEducationOrganizationId; + organizationId.ShouldBeGreaterThan(0); + + Transaction(usersContext => + { + var getApplicationsByOdsInstanceIdQuery = new GetApplicationsByOdsInstanceIdQuery(usersContext); + var results = getApplicationsByOdsInstanceIdQuery.Execute(odsInstance.OdsInstanceId); + results.Single().ApplicationEducationOrganizations.Single().ApplicationEducationOrganizationId.ShouldBe(organizationId); + }); + } + + [Test] + public void ShouldGetApplicationProfile() + { + var vendor = new Vendor { VendorName = "test vendor" }; + var odsInstance = new OdsInstance + { + Name = "Test Instance", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + var application = new Application + { + ApplicationName = "test application", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + }; + var profile = new Profile + { + Applications = new List { application }, + ProfileName = "test profile" + }; + + var user = new VendorUser + { + Email = "", + FullName = application.ApplicationName, + Vendor = vendor + }; + + var apiClient = new ApiClient + { + Application = application, + Key = "key", + Secret = "secret", + Name = application.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active", + User = user, + }; + + var apiClientOdsIntance = new ApiClientOdsInstance + { + ApiClient = apiClient, + OdsInstance = odsInstance, + }; + application.Profiles.Add(profile); + + Save(odsInstance, vendor, application, apiClient, apiClientOdsIntance); + var profileId = profile.ProfileId; + profileId.ShouldBeGreaterThan(0); + + Transaction(usersContext => + { + var getApplicationsByOdsInstanceIdQuery = new GetApplicationsByOdsInstanceIdQuery(usersContext); + var results = getApplicationsByOdsInstanceIdQuery.Execute(odsInstance.OdsInstanceId); + results.Single().Profiles.Single().ProfileId.ShouldBe(profileId); + }); + } + + [Test] + public void ShouldThrowWhenOdsInstanceIdIsInvalid() + { + Transaction(usersContext => + { + var getApplicationsByOdsInstanceIdQuery = new GetApplicationsByOdsInstanceIdQuery(usersContext); + Should.Throw>(() => + { + getApplicationsByOdsInstanceIdQuery.Execute(int.MaxValue); + }); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationsByVendorIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationsByVendorIdQueryTests.cs index e7e56e518..5dee3ec43 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationsByVendorIdQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationsByVendorIdQueryTests.cs @@ -10,6 +10,8 @@ using System.Linq; using EdFi.Admin.DataAccess.Models; using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure; namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetAuthStrategiesQueryTest.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetAuthStrategiesQueryTest.cs new file mode 100644 index 000000000..4fb4fcaef --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetAuthStrategiesQueryTest.cs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Security.DataAccess.Contexts; +using EdFi.Security.DataAccess.Models; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetAuthStrategiesQueryTest : PlatformSecurityContextTestBase +{ + protected override string ConnectionString => Testing.SecurityConnectionString; + + protected override SqlServerSecurityContext CreateDbContext() + { + return new SqlServerSecurityContext(Testing.GetDbContextOptions(ConnectionString)); + } + + [Test] + public void Should_Retrieve_AuthStrategies() + { + var newAuthStrategy = new AuthorizationStrategy + { + AuthorizationStrategyName = "Test Auth S", + DisplayName = "Test Auth Strategy", + }; + + Save(newAuthStrategy); + + Transaction(securityContext => + { + var command = new GetAuthStrategiesQuery(securityContext, Testing.GetAppSettings()); + var allAuthStrategies = command.Execute(); + + allAuthStrategies.ShouldNotBeEmpty(); + + var authStrategy = allAuthStrategies.Single(v => v.AuthorizationStrategyId == newAuthStrategy.AuthorizationStrategyId); + authStrategy.AuthorizationStrategyName.ShouldBe("Test Auth S"); + authStrategy.DisplayName.ShouldBe("Test Auth Strategy"); + }); + } + + [Test] + public void Should_Retrieve_AuthStrategies_With_Offset_And_Limit() + { + var authStrategies = new AuthorizationStrategy[5]; + + for (var index = 0; index < 5; index++) + { + authStrategies[index] = new AuthorizationStrategy + { + AuthorizationStrategyName = "Test Auth S " + (index + 1), + DisplayName = "Test Auth Strategy" + (index + 1), + }; + } + + Save(authStrategies); + + Transaction(securityContext => + { + var command = new GetAuthStrategiesQuery(securityContext, Testing.GetAppSettings()); + + var offset = 0; + var limit = 2; + + var authStrategiesAfterOffset = command.Execute(new CommonQueryParams(offset, limit, null, null)); + + authStrategiesAfterOffset.ShouldNotBeEmpty(); + authStrategiesAfterOffset.Count.ShouldBe(2); + + authStrategiesAfterOffset.ShouldContain(v => v.AuthorizationStrategyName == "Test Auth S 1"); + authStrategiesAfterOffset.ShouldContain(v => v.AuthorizationStrategyName == "Test Auth S 2"); + + offset = 2; + + authStrategiesAfterOffset = command.Execute(new CommonQueryParams(offset, limit, null, null)); + + authStrategiesAfterOffset.ShouldNotBeEmpty(); + authStrategiesAfterOffset.Count.ShouldBe(2); + + authStrategiesAfterOffset.ShouldContain(v => v.AuthorizationStrategyName == "Test Auth S 3"); + authStrategiesAfterOffset.ShouldContain(v => v.AuthorizationStrategyName == "Test Auth S 4"); + offset = 4; + + authStrategiesAfterOffset = command.Execute(new CommonQueryParams(offset, limit, null, null)); + + authStrategiesAfterOffset.ShouldNotBeEmpty(); + authStrategiesAfterOffset.Count.ShouldBe(1); + + authStrategiesAfterOffset.ShouldContain(v => v.AuthorizationStrategyName == "Test Auth S 5"); + }); + } + +} + diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetChildResourceClaimsForParentQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetChildResourceClaimsForParentQueryTests.cs index eb0dd1007..7f21984c4 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetChildResourceClaimsForParentQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetChildResourceClaimsForParentQueryTests.cs @@ -3,12 +3,11 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System.Linq; using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; using NUnit.Framework; using Shouldly; -using Application = EdFi.Security.DataAccess.Models.Application; +using System.Linq; namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; @@ -18,18 +17,11 @@ public class GetChildResourceClaimsForParentQueryTests : SecurityDataTestBase [Test] public void ShouldGetResourceClaims() { - var testApplication = new Application - { - ApplicationName = "TestApplicationName" - }; - - Save(testApplication); - var parentRcs = UniqueNameList("Parent", 2); var childRcs = UniqueNameList("Child", 1); - var testResourceClaims = SetupResourceClaims(testApplication, parentRcs, childRcs); + var testResourceClaims = SetupResourceClaims(parentRcs, childRcs); var testParentResource = testResourceClaims.Single(x => x.ResourceName == parentRcs.First()); @@ -49,10 +41,7 @@ public void ShouldGetResourceClaims() results.Length.ShouldBe(testChildResourceClaims.Count()); results.Select(x => x.Name).ShouldBe(testChildResourceClaims.Select(x => x.ResourceName), true); results.Select(x => x.Id).ShouldBe(testChildResourceClaims.Select(x => x.ResourceClaimId), true); - results.All(x => x.Create == false).ShouldBe(true); - results.All(x => x.Delete == false).ShouldBe(true); - results.All(x => x.Update == false).ShouldBe(true); - results.All(x => x.Read == false).ShouldBe(true); + results.All(x => x.Actions == null).ShouldBeTrue(); results.All(x => x.ParentId.Equals(testParentResource.ResourceClaimId)).ShouldBe(true); }); } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceContextByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceContextByIdQueryTests.cs new file mode 100644 index 000000000..62ca55355 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceContextByIdQueryTests.cs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetOdsInstanceContextByIdQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void Should_Retreive_OdsInstanceContext() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var odsInstanceContext1 = new OdsInstanceContext + { + ContextKey = "contextKey", + ContextValue = "contextValue", + OdsInstance = odsInstance + }; + + var odsInstanceContext2 = new OdsInstanceContext + { + ContextKey = "contextKey2", + ContextValue = "contextValue2", + OdsInstance = odsInstance + }; + + Save(odsInstance, odsInstanceContext1, odsInstanceContext2); + + OdsInstanceContext result = null; + Transaction(usersContext => + { + var query = new GetOdsInstanceContextByIdQuery(usersContext); + result = query.Execute(odsInstanceContext1.OdsInstanceContextId); + result.ShouldNotBeNull(); + result.OdsInstanceContextId.ShouldBe(odsInstanceContext1.OdsInstanceContextId); + result.OdsInstance.OdsInstanceId.ShouldBe(odsInstanceContext1.OdsInstance.OdsInstanceId); + result.ContextKey.ShouldBe(odsInstanceContext1.ContextKey); + result.ContextValue.ShouldBe(odsInstanceContext1.ContextValue); + }); + } + +} + diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceDerivativeByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceDerivativeByIdQueryTests.cs new file mode 100644 index 000000000..f95b92e40 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceDerivativeByIdQueryTests.cs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetOdsInstanceDerivativeByIdQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void Should_Retreive_OdsInstanceDerivative() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var odsInstanceDerivative1 = new OdsInstanceDerivative + { + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False", + DerivativeType = "ReadReplica", + OdsInstance = odsInstance + }; + + var odsInstanceDerivative2 = new OdsInstanceDerivative + { + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods2;Integrated Security=True;Encrypt=False", + DerivativeType = "Snapshot", + OdsInstance = odsInstance + }; + + Save(odsInstance, odsInstanceDerivative1, odsInstanceDerivative2); + + OdsInstanceDerivative result = null; + Transaction(usersContext => + { + var query = new GetOdsInstanceDerivativeByIdQuery(usersContext); + result = query.Execute(odsInstanceDerivative1.OdsInstanceDerivativeId); + result.ShouldNotBeNull(); + result.OdsInstanceDerivativeId.ShouldBe(odsInstanceDerivative1.OdsInstanceDerivativeId); + result.OdsInstance.OdsInstanceId.ShouldBe(odsInstanceDerivative1.OdsInstance.OdsInstanceId); + result.DerivativeType.ShouldBe(odsInstanceDerivative1.DerivativeType); + result.ConnectionString.ShouldBe(odsInstanceDerivative1.ConnectionString); + }); + } + +} + diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesByIdQueryTests.cs new file mode 100644 index 000000000..aa54ffdee --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesByIdQueryTests.cs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetOdsInstanceByIdQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldGetInstanceById() + { + + Transaction(usersContext => + { + var odsInstance = new OdsInstance + { + InstanceType = "test type", + Name = "test ods instance 1", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + Save(odsInstance); + var command = new GetOdsInstanceQuery(usersContext); + var result = command.Execute(odsInstance.OdsInstanceId); + result.OdsInstanceId.ShouldBe(odsInstance.OdsInstanceId); + result.Name.ShouldBe("test ods instance 1"); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesContextQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesContextQueryTests.cs new file mode 100644 index 000000000..f08693d80 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesContextQueryTests.cs @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetOdsInstancesContextQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void Should_Retreive_OdsInstancesContext() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var odsInstanceContext1 = new OdsInstanceContext + { + ContextKey = "contextKey", + ContextValue = "contextValue", + OdsInstance = odsInstance + }; + + Save(odsInstance, odsInstanceContext1); + + List results = null; + Transaction(usersContext => + { + var query = new GetOdsInstanceContextsQuery(usersContext, Testing.GetAppSettings()); + results = query.Execute(); + }); + + results.Any(p => p.OdsInstance.OdsInstanceId == odsInstanceContext1.OdsInstance.OdsInstanceId).ShouldBeTrue(); + results.Any(p => p.ContextKey == odsInstanceContext1.ContextKey).ShouldBeTrue(); + results.Any(p => p.ContextValue == odsInstanceContext1.ContextValue).ShouldBeTrue(); + + } + + [Test] + public void Should_Retreive_OdsInstancesContext_With_Offset_Limit() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var odsInstanceContext1 = new OdsInstanceContext + { + ContextKey = "contextKey", + ContextValue = "contextValue", + OdsInstance = odsInstance + }; + + var odsInstanceContext2 = new OdsInstanceContext + { + ContextKey = "contextKey2", + ContextValue = "contextValue2", + OdsInstance = odsInstance + }; + + Save(odsInstance, odsInstanceContext1, odsInstanceContext2); + + List results = null; + Transaction(usersContext => + { + var query = new GetOdsInstanceContextsQuery(usersContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(1, 1)); + results.Count.ShouldBe(1); + }); + + results.Any(p => p.OdsInstance.OdsInstanceId == odsInstanceContext2.OdsInstance.OdsInstanceId).ShouldBeTrue(); + results.Any(p => p.ContextKey == odsInstanceContext2.ContextKey).ShouldBeTrue(); + results.Any(p => p.ContextValue == odsInstanceContext2.ContextValue).ShouldBeTrue(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesDerivativeQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesDerivativeQueryTests.cs new file mode 100644 index 000000000..5ae7c33f9 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesDerivativeQueryTests.cs @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetOdsInstancesDerivativeQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void Should_Retreive_OdsInstancesDerivative() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var odsInstanceDerivative1 = new OdsInstanceDerivative + { + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False", + DerivativeType = "ReadReplica", + OdsInstance = odsInstance + }; + + Save(odsInstance, odsInstanceDerivative1); + + List results = null; + Transaction(usersContext => + { + var query = new GetOdsInstanceDerivativesQuery(usersContext, Testing.GetAppSettings()); + results = query.Execute(); + }); + + results.Any(p => p.OdsInstance.OdsInstanceId == odsInstanceDerivative1.OdsInstance.OdsInstanceId).ShouldBeTrue(); + results.Any(p => p.DerivativeType == odsInstanceDerivative1.DerivativeType).ShouldBeTrue(); + results.Any(p => p.ConnectionString == odsInstanceDerivative1.ConnectionString).ShouldBeTrue(); + + } + + [Test] + public void Should_Retreive_OdsInstancesDerivative_With_Offset_Limit() + { + var odsInstance = new OdsInstance + { + Name = "ODS Instance Name", + InstanceType = "Ods", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + + var odsInstanceDerivative1 = new OdsInstanceDerivative + { + ConnectionString = "{ConnectionString}", + DerivativeType = "Type1", + OdsInstance = odsInstance + }; + + var odsInstanceDerivative2 = new OdsInstanceDerivative + { + ConnectionString = "{ConnectionString}", + DerivativeType = "Type2", + OdsInstance = odsInstance + }; + + Save(odsInstance, odsInstanceDerivative1, odsInstanceDerivative2); + + List results = null; + Transaction(usersContext => + { + var query = new GetOdsInstanceDerivativesQuery(usersContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(1, 1)); + results.Count.ShouldBe(1); + }); + + results.Any(p => p.OdsInstance.OdsInstanceId == odsInstanceDerivative2.OdsInstance.OdsInstanceId).ShouldBeTrue(); + results.Any(p => p.DerivativeType == odsInstanceDerivative2.DerivativeType).ShouldBeTrue(); + results.Any(p => p.ConnectionString == odsInstanceDerivative2.ConnectionString).ShouldBeTrue(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesQueryTests.cs index 6cba390b0..948d695fe 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstancesQueryTests.cs @@ -3,12 +3,12 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; using NUnit.Framework; using Shouldly; -using System.Linq; -using EdFi.Admin.DataAccess.Models; -using EdFi.Ods.AdminApi.Infrastructure; namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; @@ -16,79 +16,186 @@ namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; public class GetOdsInstancesQueryTests : PlatformUsersContextTestBase { [Test] - public void Should_retrieve_ods_instances() + public void ShouldGetAllInstances() { - var newOdsInstance = new OdsInstance + Transaction(usersContext => { - Name = "test ods instance", - InstanceType = "test ods instance type", - Status = "OK", - Version = "1.0.0", - }; - - Save(newOdsInstance); + CreateMultiple(2); + var command = new GetOdsInstancesQuery(usersContext, Testing.GetAppSettings()); + var results = command.Execute(); + results.Count.ShouldBe(2); + }); + } + [Test] + public void ShouldGetAllInstancesWithOffsetAndLimit() + { Transaction(usersContext => { + CreateMultiple(); + var offset = 0; + var limit = 2; + var command = new GetOdsInstancesQuery(usersContext, Testing.GetAppSettings()); - var allOdsInstances = command.Execute(); + var odsInstancesAfterOffset = command.Execute(new CommonQueryParams(offset, limit), null, null, null); - allOdsInstances.ShouldNotBeEmpty(); + odsInstancesAfterOffset.ShouldNotBeEmpty(); + odsInstancesAfterOffset.Count.ShouldBe(2); + + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 1"); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 2"); - var odsInstance = allOdsInstances.Single(v => v.OdsInstanceId == newOdsInstance.OdsInstanceId); - odsInstance.Name.ShouldBe("test ods instance"); - odsInstance.InstanceType.ShouldBe("test ods instance type"); + offset = 2; + + odsInstancesAfterOffset = command.Execute(new CommonQueryParams(offset, limit), null, null, null); + + odsInstancesAfterOffset.ShouldNotBeEmpty(); + odsInstancesAfterOffset.Count.ShouldBe(2); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 3"); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 4"); + + offset = 4; + + odsInstancesAfterOffset = command.Execute(new CommonQueryParams(offset, limit), null, null, null); + + odsInstancesAfterOffset.ShouldNotBeEmpty(); + odsInstancesAfterOffset.Count.ShouldBe(1); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 5"); }); } [Test] - public void Should_retrieve_ods_instances_with_offset_and_limit() + public void ShouldGetAllInstancesWithoutOffsetAndLimit() { - var odsInstances = new OdsInstance[5]; - - for (var odsInstanceIndex = 0; odsInstanceIndex < 5; odsInstanceIndex++) + Transaction(usersContext => { - odsInstances[odsInstanceIndex] = new OdsInstance - { - Name = $"test ods instance {odsInstanceIndex + 1}", - InstanceType = "test ods instance type", - Status = "OK", - Version = "1.0.0", - }; - } + CreateMultiple(); - Save(odsInstances); + var command = new GetOdsInstancesQuery(usersContext, Testing.GetAppSettings()); + var odsInstancesAfterOffset = command.Execute(new CommonQueryParams(), null, null, null); + + odsInstancesAfterOffset.ShouldNotBeEmpty(); + odsInstancesAfterOffset.Count.ShouldBe(5); + + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 1"); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 2"); + odsInstancesAfterOffset = command.Execute(new CommonQueryParams(), null, null, null); + + odsInstancesAfterOffset.ShouldNotBeEmpty(); + odsInstancesAfterOffset.Count.ShouldBe(5); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 3"); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 4"); + + odsInstancesAfterOffset = command.Execute(new CommonQueryParams(), null, null, null); + + odsInstancesAfterOffset.ShouldNotBeEmpty(); + odsInstancesAfterOffset.Count.ShouldBe(5); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 5"); + }); + } + [Test] + public void ShouldGetAllInstancesWithoutLimit() + { Transaction(usersContext => { + CreateMultiple(); + var offset = 0; + var command = new GetOdsInstancesQuery(usersContext, Testing.GetAppSettings()); - var commonQueryParams = new CommonQueryParams(0, 2); + var odsInstancesAfterOffset = command.Execute(new CommonQueryParams(offset, null), null, null, null); - var odsInstancesAfterOffset = command.Execute(commonQueryParams); + odsInstancesAfterOffset.ShouldNotBeEmpty(); + odsInstancesAfterOffset.Count.ShouldBe(5); + + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 1"); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 2"); + + offset = 2; + + odsInstancesAfterOffset = command.Execute(new CommonQueryParams(offset, null), null, null, null); odsInstancesAfterOffset.ShouldNotBeEmpty(); - odsInstancesAfterOffset.Count.ShouldBe(2); + odsInstancesAfterOffset.Count.ShouldBe(3); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 3"); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 4"); + + offset = 4; - odsInstancesAfterOffset.ShouldContain(v => v.Name == "test ods instance 1"); - odsInstancesAfterOffset.ShouldContain(v => v.Name == "test ods instance 2"); + odsInstancesAfterOffset = command.Execute(new CommonQueryParams(offset, null), null, null, null); - commonQueryParams.Offset = 2; + odsInstancesAfterOffset.ShouldNotBeEmpty(); + odsInstancesAfterOffset.Count.ShouldBe(1); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 5"); + }); + } + + [Test] + public void ShouldGetAllInstancesWithoutOffset() + { + Transaction(usersContext => + { + CreateMultiple(); + var limit = 2; - odsInstancesAfterOffset = command.Execute(commonQueryParams); + var command = new GetOdsInstancesQuery(usersContext, Testing.GetAppSettings()); + var odsInstancesAfterOffset = command.Execute(new CommonQueryParams(null, limit), null, null, null); odsInstancesAfterOffset.ShouldNotBeEmpty(); odsInstancesAfterOffset.Count.ShouldBe(2); - odsInstancesAfterOffset.ShouldContain(v => v.Name == "test ods instance 3"); - odsInstancesAfterOffset.ShouldContain(v => v.Name == "test ods instance 4"); - commonQueryParams.Offset = 4; + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 1"); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == "test ods instance 2"); + }); + } + + [Test] + public void ShouldGetAllInstancesWithId() + { + Transaction(usersContext => + { + var odsInstances = CreateMultiple(); + var command = new GetOdsInstancesQuery(usersContext, Testing.GetAppSettings()); + var odsInstancesAfterOffset = command.Execute(new CommonQueryParams(), odsInstances[2].OdsInstanceId, null, null); + + odsInstancesAfterOffset.ShouldNotBeEmpty(); + odsInstancesAfterOffset.Count.ShouldBe(1); - odsInstancesAfterOffset = command.Execute(commonQueryParams); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == odsInstances[2].Name); + }); + } + + [Test] + public void ShouldGetAllInstancesWithName() + { + Transaction(usersContext => + { + var odsInstances = CreateMultiple(); + var command = new GetOdsInstancesQuery(usersContext, Testing.GetAppSettings()); + var odsInstancesAfterOffset = command.Execute(new CommonQueryParams(), null, odsInstances[2].Name, null); odsInstancesAfterOffset.ShouldNotBeEmpty(); odsInstancesAfterOffset.Count.ShouldBe(1); - odsInstancesAfterOffset.ShouldContain(v => v.Name == "test ods instance 5"); + odsInstancesAfterOffset.ShouldContain(odsI => odsI.Name == odsInstances[2].Name); }); } + + private static OdsInstance[] CreateMultiple(int total = 5) + { + var odsInstances = new OdsInstance[total]; + + for (var odsIndex = 0; odsIndex < total; odsIndex++) + { + odsInstances[odsIndex] = new OdsInstance + { + InstanceType = "test type", + Name = $"test ods instance {odsIndex + 1}", + ConnectionString = "Data Source=(local);Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=False" + }; + } + Save(odsInstances); + + return odsInstances; + } } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetProfileByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetProfileByIdQueryTests.cs new file mode 100644 index 000000000..68af413aa --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetProfileByIdQueryTests.cs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetProfileByIdQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void Should_retreive_profile() + { + var profile1 = CreateProfile(); + var profile2 = CreateProfile(); + + Save(profile1, profile2); + + Profile result = null; + Transaction(usersContext => + { + var query = new GetProfileByIdQuery(usersContext); + result = query.Execute(profile2.ProfileId); + result.ShouldNotBeNull(); + result.ProfileId.ShouldBe(profile2.ProfileId); + result.ProfileName.ShouldBe(profile2.ProfileName); + }); + } + + private static int _profileId = 0; + private static Profile CreateProfile() + { + return new Profile + { + ProfileName = $"Test Profile {_profileId++}-{DateTime.Now:O}" + }; + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetProfilesQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetProfilesQueryTests.cs index 6a888e9d5..1991df713 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetProfilesQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetProfilesQueryTests.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.Linq; using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; using NUnit.Framework; using Shouldly; @@ -27,7 +29,7 @@ public void Should_retreive_profiles() List results = null; Transaction(usersContext => { - var query = new GetProfilesQuery(usersContext); + var query = new GetProfilesQuery(usersContext, Testing.GetAppSettings()); results = query.Execute(); }); @@ -43,4 +45,59 @@ private static Profile CreateProfile() ProfileName = $"Test Profile {_profileId++}-{DateTime.Now:O}" }; } + + [Test] + public void Should_retreive_profiles_with_offset_limit() + { + var profile1 = CreateProfile(); + var profile2 = CreateProfile(); + var profile3 = CreateProfile(); + + Save(profile1, profile2, profile3); + + List results = null; + Transaction(usersContext => + { + var query = new GetProfilesQuery(usersContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(1, 1), null, null); + results.Count.ShouldBe(1); + }); + results.Any(p => p.ProfileName == profile2.ProfileName).ShouldBeTrue(); + } + + [Test] + public void Should_retreive_profiles_with_id() + { + var profile1 = CreateProfile(); + var profile2 = CreateProfile(); + + Save(profile1, profile2); + + List results = null; + Transaction(usersContext => + { + var query = new GetProfilesQuery(usersContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(0, 5), profile2.ProfileId, null); + results.Count.ShouldBe(1); + }); + results.Any(p => p.ProfileName == profile2.ProfileName).ShouldBeTrue(); + } + + [Test] + public void Should_retreive_profiles_with_name() + { + var profile1 = CreateProfile(); + var profile2 = CreateProfile(); + + Save(profile1, profile2); + + List results = null; + Transaction(usersContext => + { + var query = new GetProfilesQuery(usersContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(0, 5), null, profile2.ProfileName); + results.Count.ShouldBe(1); + }); + results.Any(p => p.ProfileName == profile2.ProfileName).ShouldBeTrue(); + } } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimActionsQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimActionsQueryTests.cs new file mode 100644 index 000000000..8c503e8d6 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimActionsQueryTests.cs @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Features.ResourceClaimActions; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Security.DataAccess.Models; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetResourceClaimActionsQueryTests : SecurityDataTestBase +{ + [Test] + public void ShouldGetResourceClaimActions() + { + var skip = 0; + ResourceClaimActionModel[] results = null; + using var securityContext = TestContext; + var actions = SetupActions().Select(s => s.ActionId).ToArray(); + var resourceClaimId = SetupResourceClaims().FirstOrDefault().ResourceClaimId; + var testResourceClaimActions = SetupResourceClaimActions(actions, resourceClaimId); + var query = new GetResourceClaimActionsQuery(securityContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(skip, Testing.DefaultPageSizeLimit), null).ToArray(); + results.SelectMany(x => x.Actions).Count().ShouldBe(testResourceClaimActions.Count); + results.Select(x => x.ResourceClaimId).ShouldBe(testResourceClaimActions.Select(s => s.ResourceClaimId).Distinct(), true); + results.Select(x => x.ResourceName).ShouldBe(testResourceClaimActions.Select(x => x.ResourceClaim.ResourceName).Distinct(), true); + } + + [Test] + public void ShouldGetAllResourceClaimActions_With_Offset_and_Limit() + { + var offset = 1; + var limit = 2; + + ResourceClaimActionModel[] results = null; + using var securityContext = TestContext; + //Set actions + var actions = SetupActions().Select(s => s.ActionId).ToArray(); + //Set resourceClaims + var resourceClaims = SetupResourceClaims(4); + + foreach (var resourceClaim in resourceClaims) + { + var testResourceClaimActions = SetupResourceClaimActions(actions, resourceClaim.ResourceClaimId); + } + //Add ResourceClaimActions + var query = new GetResourceClaimActionsQuery(securityContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(offset, limit), null).ToArray(); + + results.Length.ShouldBe(2); + results[0].ResourceName.ShouldBe("TestResourceClaim2.00"); + results[1].ResourceName.ShouldBe("TestResourceClaim3.00"); + results[0].Actions.Any().ShouldBe(true); + results[1].Actions.Any().ShouldBe(true); + } + + [Test] + public void ShouldGetResourceClaimActionWhitResourceNameFilter() + { + var skip = 0; + ResourceClaimActionModel[] results = null; + using var securityContext = TestContext; + var actions = SetupActions().Select(s => s.ActionId).ToArray(); + var resourceClaim = SetupResourceClaims().FirstOrDefault(); + var resourceClaimId = resourceClaim.ResourceClaimId; + var resourceName = resourceClaim.ResourceName; + + var testResourceClaimActions = SetupResourceClaimActions(actions, resourceClaimId); + var query = new GetResourceClaimActionsQuery(securityContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(skip, Testing.DefaultPageSizeLimit), resourceName).ToArray(); + results.SelectMany(x => x.Actions).Count().ShouldBe(testResourceClaimActions.Count); + results.Select(x => x.ResourceClaimId).ShouldBe(new[] { resourceClaimId }); + results.Select(x => x.ResourceName).ShouldBe(new[] { resourceName }); + } + + [Test] + public void ShouldGetResourceClaimActionWhitIncorrectResourceNameFilter() + { + var skip = 0; + ResourceClaimActionModel[] results = null; + using var securityContext = TestContext; + var actions = SetupActions().Select(s => s.ActionId).ToArray(); + var resourceClaim = SetupResourceClaims().FirstOrDefault(); + var resourceClaimId = resourceClaim.ResourceClaimId; + var resourceName = "Non-existing filter"; + + var testResourceClaimActions = SetupResourceClaimActions(actions, resourceClaimId); + var query = new GetResourceClaimActionsQuery(securityContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(skip, Testing.DefaultPageSizeLimit), resourceName).ToArray(); + results.SelectMany(x => x.Actions).Count().ShouldBe(0); + results.Select(x => x.ResourceClaimId).ShouldBeEmpty(); + results.Select(x => x.ResourceName).ShouldBeEmpty(); + } + + private IReadOnlyCollection SetupResourceClaimActions(int[] actions, int resourceClaimId) + { + var resourceClaimActions = new List(); + var resourceClaimCount = actions.Length; + foreach (var index in Enumerable.Range(1, resourceClaimCount)) + { + var resourceClaim = new ResourceClaimAction + { + ActionId = actions[index - 1], + ResourceClaimId = resourceClaimId, + ValidationRuleSetName = $"Test{index}" + }; + resourceClaimActions.Add(resourceClaim); + } + Save(resourceClaimActions.Cast().ToArray()); + return resourceClaimActions; + } + + private IReadOnlyCollection SetupResourceClaims(int resourceClaimCount = 1) + { + var resourceClaims = new List(); + foreach (var index in Enumerable.Range(1, resourceClaimCount)) + { + var resourceClaim = new ResourceClaim + { + ClaimName = $"TestResourceClaim{index:N}", + ResourceName = $"TestResourceClaim{index:N}", + }; + resourceClaims.Add(resourceClaim); + } + + Save(resourceClaims.Cast().ToArray()); + + return resourceClaims; + } + + private IReadOnlyCollection SetupActions(int resourceClaimCount = 5) + { + var actions = new List(); + foreach (var index in Enumerable.Range(1, resourceClaimCount)) + { + var action = new Security.DataAccess.Models.Action + { + ActionName = $"TestResourceClaim{index:N}", + ActionUri = $"http://ed-fi.org/odsapi/actions/TestResourceClaim{index:N}" + }; + actions.Add(action); + } + + Save(actions.Cast().ToArray()); + + return actions; + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimByResourceClaimIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimByResourceClaimIdQueryTests.cs new file mode 100644 index 000000000..43e329530 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimByResourceClaimIdQueryTests.cs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Security.DataAccess.Contexts; +using EdFi.Security.DataAccess.Models; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; + +[TestFixture] +public class GetResourceClaimByResourceClaimIdQueryTests : SecurityDataTestBase +{ + [Test] + public void ShouldGetResourceClaimByResourceClaimId() + { + Transaction(usersContext => + { + var testResourceClaims = SetupResourceClaims(usersContext); + var query = new GetResourceClaimByResourceClaimIdQuery(usersContext); + + var result = query.Execute(testResourceClaims.ResourceClaimId); + + result.Name.ShouldBe(testResourceClaims.ResourceName); + + }); + + + } + + [Test] + public void ShouldGetResourceClaimChildrenByResourceClaimId() + { + Transaction(usersContext => + { + var testResourceClaims = SetupResourceClaims(usersContext); + var query = new GetResourceClaimByResourceClaimIdQuery(usersContext); + + var result = query.Execute(testResourceClaims.ResourceClaimId); + + result.Children.Count.ShouldBe(1); + result.Children.ShouldAllBe(p => p.ParentId == testResourceClaims.ResourceClaimId); + }); + + + } + + private ResourceClaim SetupResourceClaims(ISecurityContext usersContext) + { + var parentResourceClaim = new ResourceClaim() + { + ClaimName = $"ParentTestResourceClaim", + ResourceName = $"ParentTestResourceClaim", + }; + usersContext.ResourceClaims.Add(parentResourceClaim); + Save(parentResourceClaim); + + var resourceClaim = new ResourceClaim() + { + ClaimName = $"ChildrenTestResourceClaim", + ResourceName = $"ChildrenTestResourceClaim", + ParentResourceClaimId = parentResourceClaim.ResourceClaimId, + }; + Save(resourceClaim); + + return parentResourceClaim; + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimsAsFlatListQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimsAsFlatListQueryTests.cs index 4f8121a10..6cdea6c1c 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimsAsFlatListQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimsAsFlatListQueryTests.cs @@ -10,9 +10,6 @@ using NUnit.Framework; using Shouldly; -using Application = EdFi.Security.DataAccess.Models.Application; -using ResourceClaim = EdFi.Security.DataAccess.Models.ResourceClaim; - namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; [TestFixture] @@ -21,14 +18,17 @@ public class GetResourceClaimsAsFlatListQueryTests : SecurityDataTestBase [Test] public void ShouldGetResourceClaimsAsFlatList() { - var testApplication = new Application - { - ApplicationName = "TestApplicationName" - }; - - Save(testApplication); - - var testResourceClaims = SetupResourceClaims(testApplication); + var parentPrefix = "ParentRc"; + var childPrefix = "ChildRc"; + var grandChildPrefix = "GrandChildRc"; + var parentResourceNames = UniqueNameList(parentPrefix, 3); + var childrenResourceNames = UniqueNameList(childPrefix, 2); + var grandChildResourceNames = UniqueNameList(grandChildPrefix, 2); + + var testResourceClaims = SetupResourceClaimsWithChildren( + parentResourceNames, + childrenResourceNames, + grandChildResourceNames); Infrastructure.ClaimSetEditor.ResourceClaim[] results = null; using var securityContext = TestContext; @@ -37,29 +37,25 @@ public void ShouldGetResourceClaimsAsFlatList() results.Length.ShouldBe(testResourceClaims.Count); results.Select(x => x.Name).ShouldBe(testResourceClaims.Select(x => x.ResourceName), true); results.Select(x => x.Id).ShouldBe(testResourceClaims.Select(x => x.ResourceClaimId), true); - results.All(x => x.Create == false).ShouldBe(true); - results.All(x => x.Delete == false).ShouldBe(true); - results.All(x => x.Update == false).ShouldBe(true); - results.All(x => x.Read == false).ShouldBe(true); - results.All(x => x.ParentId.Equals(0)).ShouldBe(true); - results.All(x => x.ParentName == null).ShouldBe(true); - results.All(x => x.Children.Count == 0).ShouldBe(true); + results.All(x => x.Actions == null).ShouldBe(true); + //Assert parent Resource Claims + results.Count(x => x.ParentId.Equals(0)).ShouldBe(parentResourceNames.Count); + results.Count(x => x.Name.StartsWith(parentPrefix)).ShouldBe(parentResourceNames.Count); + //Assert child Resource Claims + results.Count(x => x.Name.StartsWith(childPrefix)).ShouldBe(parentResourceNames.Count * childrenResourceNames.Count); + //Assert grandchild Resource Claims + results.Count(x => x.Name.StartsWith(grandChildPrefix)).ShouldBe(parentResourceNames.Count * childrenResourceNames.Count * grandChildResourceNames.Count); + } + [Test] public void ShouldGetAlphabeticallySortedFlatListForResourceClaims() { - var testApplication = new Application - { - ApplicationName = "TestApplicationName" - }; - - Save(testApplication); - var testClaimSet = new ClaimSet - { ClaimSetName = "TestClaimSet_test", Application = testApplication }; + { ClaimSetName = "TestClaimSet_test" }; Save(testClaimSet); - var testResourceClaims = SetupParentResourceClaimsWithChildren(testClaimSet, testApplication, UniqueNameList("ParentRc", 3), UniqueNameList("ChildRc", 1)).ToList(); + var testResourceClaims = SetupClaimSetResourceClaimActions(testClaimSet, UniqueNameList("ParentRc", 3), UniqueNameList("ChildRc", 1)).ToList(); var parentResourceNames = testResourceClaims.Where(x => x.ResourceClaim?.ParentResourceClaim == null) .OrderBy(x => x.ResourceClaim.ResourceName).Select(x => x.ResourceClaim?.ResourceName).ToList(); var childResourceNames = testResourceClaims.Where(x => x.ResourceClaim?.ParentResourceClaim != null) @@ -74,48 +70,4 @@ public void ShouldGetAlphabeticallySortedFlatListForResourceClaims() results.Where(x => x.ParentId != 0).Select(x => x.Name).ToList().ShouldBe(childResourceNames); } - private IReadOnlyCollection SetupResourceClaims(Application testApplication, int resourceClaimCount = 5) - { - var resourceClaims = new List(); - foreach (var index in Enumerable.Range(1, resourceClaimCount)) - { - var resourceClaim = new ResourceClaim - { - ClaimName = $"TestResourceClaim{index:N}", - DisplayName = $"TestResourceClaim{index:N}", - ResourceName = $"TestResourceClaim{index:N}", - Application = testApplication - }; - resourceClaims.Add(resourceClaim); - } - - Save(resourceClaims.Cast().ToArray()); - - return resourceClaims; - } - - //private IReadOnlyCollection SetupParentResourceClaimsWithChildren(Application testApplication, int resourceClaimCount = 5, int childResourceClaimCount = 3) - //{ - // var parentResourceClaims = Enumerable.Range(1, resourceClaimCount).Select(parentIndex => new ResourceClaim - // { - // ClaimName = $"TestParentResourceClaim{parentIndex}", - // DisplayName = $"TestParentResourceClaim{parentIndex}", - // ResourceName = $"TestParentResourceClaim{parentIndex}", - // Application = testApplication - // }).ToList(); - - // var childResourceClaims = parentResourceClaims.SelectMany(x => Enumerable.Range(1, childResourceClaimCount) - // .Select(childIndex => new ResourceClaim - // { - // ClaimName = $"TestChildResourceClaim{childIndex}", - // DisplayName = $"TestChildResourceClaim{childIndex}", - // ResourceName = $"TestChildResourceClaim{childIndex}", - // Application = testApplication, - // ParentResourceClaim = x - // })).ToList(); - - // Save(childResourceClaims.Cast().ToArray()); - // parentResourceClaims.AddRange(childResourceClaims); - // return parentResourceClaims; - //} } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimsQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimsQueryTests.cs index 311180bec..baad87d27 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimsQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetResourceClaimsQueryTests.cs @@ -5,10 +5,10 @@ using System.Collections.Generic; using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; using NUnit.Framework; using Shouldly; -using Application = EdFi.Security.DataAccess.Models.Application; using ResourceClaim = EdFi.Security.DataAccess.Models.ResourceClaim; namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; @@ -19,32 +19,113 @@ public class GetResourceClaimsQueryTests : SecurityDataTestBase [Test] public void ShouldGetResourceClaims() { - var testApplication = new Application - { - ApplicationName = "TestApplicationName" - }; - - Save(testApplication); - - var testResourceClaims = SetupResourceClaims(testApplication); + var testResourceClaims = SetupResourceClaims(); Infrastructure.ClaimSetEditor.ResourceClaim[] results = null; using var securityContext = TestContext; - var query = new GetResourceClaimsQuery(securityContext); + var query = new GetResourceClaimsQuery(securityContext, Testing.GetAppSettings()); results = query.Execute().ToArray(); results.Length.ShouldBe(testResourceClaims.Count); results.Select(x => x.Name).ShouldBe(testResourceClaims.Select(x => x.ResourceName), true); results.Select(x => x.Id).ShouldBe(testResourceClaims.Select(x => x.ResourceClaimId), true); - results.All(x => x.Create == false).ShouldBe(true); - results.All(x => x.Delete == false).ShouldBe(true); - results.All(x => x.Update == false).ShouldBe(true); - results.All(x => x.Read == false).ShouldBe(true); + results.All(x => x.Actions == null).ShouldBe(true); + results.All(x => x.ParentId.Equals(0)).ShouldBe(true); + results.All(x => x.ParentName == null).ShouldBe(true); + results.All(x => x.Children.Count == 0).ShouldBe(true); + } + + [Test] + public void ShouldGetResourceClaimsWithOffset() + { + var skip = 3; + var testResourceClaims = SetupResourceClaims(); + var testResourceClaimsResult = testResourceClaims.Skip(skip); + + Infrastructure.ClaimSetEditor.ResourceClaim[] results = null; + using var securityContext = TestContext; + var query = new GetResourceClaimsQuery(securityContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(skip, Testing.DefaultPageSizeLimit), null, null).ToArray(); + results.Length.ShouldBe(2); + results.Select(x => x.Name).ShouldBe(testResourceClaimsResult.Select(x => x.ResourceName), true); + results.Select(x => x.Id).ShouldBe(testResourceClaimsResult.Select(x => x.ResourceClaimId), true); + results.All(x => x.Actions == null).ShouldBe(true); + results.All(x => x.ParentId.Equals(0)).ShouldBe(true); + results.All(x => x.ParentName == null).ShouldBe(true); + results.All(x => x.Children.Count == 0).ShouldBe(true); + } + + [Test] + public void ShouldGetResourceClaimsWithLimit() + { + var limit = 3; + var testResourceClaims = SetupResourceClaims(); + var testResourceClaimsResult = testResourceClaims.Take(limit); + + Infrastructure.ClaimSetEditor.ResourceClaim[] results = null; + using var securityContext = TestContext; + var query = new GetResourceClaimsQuery(securityContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(Testing.DefaultPageSizeOffset, limit), null, null).ToArray(); + results.Length.ShouldBe(3); + results.Select(x => x.Name).ShouldBe(testResourceClaimsResult.Select(x => x.ResourceName), true); + results.Select(x => x.Id).ShouldBe(testResourceClaimsResult.Select(x => x.ResourceClaimId), true); + results.All(x => x.Actions == null).ShouldBe(true); results.All(x => x.ParentId.Equals(0)).ShouldBe(true); results.All(x => x.ParentName == null).ShouldBe(true); results.All(x => x.Children.Count == 0).ShouldBe(true); } - private IReadOnlyCollection SetupResourceClaims(Application testApplication, int resourceClaimCount = 5) + [Test] + public void ShouldGetResourceClaimsWithOffsetAndLimit() + { + var offset = 2; + var limit = 2; + var testResourceClaims = SetupResourceClaims(); + var testResourceClaimsResult = testResourceClaims.Skip(offset).Take(limit); + + Infrastructure.ClaimSetEditor.ResourceClaim[] results = null; + using var securityContext = TestContext; + var query = new GetResourceClaimsQuery(securityContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(offset, limit), null, null).ToArray(); + results.Length.ShouldBe(2); + results.Select(x => x.Name).ShouldBe(testResourceClaimsResult.Select(x => x.ResourceName), true); + results.Select(x => x.Id).ShouldBe(testResourceClaimsResult.Select(x => x.ResourceClaimId), true); + results.All(x => x.Actions == null).ShouldBe(true); + results.All(x => x.ParentId.Equals(0)).ShouldBe(true); + results.All(x => x.ParentName == null).ShouldBe(true); + results.All(x => x.Children.Count == 0).ShouldBe(true); + } + + [Test] + public void ShouldGetResourceClaimsWithId() + { + var name = $"TestResourceClaim{2:N}"; + var testResourceClaims = SetupResourceClaims(); + var testResourceClaimsResult = testResourceClaims.First(c => c.ResourceName == name); + + Infrastructure.ClaimSetEditor.ResourceClaim[] results = null; + using var securityContext = TestContext; + var query = new GetResourceClaimsQuery(securityContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(), testResourceClaimsResult.ResourceClaimId, null).ToArray(); + results.Length.ShouldBe(1); + results.First().Id.ShouldBe(testResourceClaimsResult.ResourceClaimId); + } + + [Test] + public void ShouldGetResourceClaimsWithName() + { + var name = $"TestResourceClaim{2:N}"; + var testResourceClaims = SetupResourceClaims(); + var testResourceClaimsResult = testResourceClaims.Where(c => c.ResourceName == name); + + Infrastructure.ClaimSetEditor.ResourceClaim[] results = null; + using var securityContext = TestContext; + var query = new GetResourceClaimsQuery(securityContext, Testing.GetAppSettings()); + results = query.Execute(new CommonQueryParams(), null, name).ToArray(); + results.Length.ShouldBe(1); + results.First().Name.ShouldBe(testResourceClaimsResult.First().ResourceName); + } + + private IReadOnlyCollection SetupResourceClaims(int resourceClaimCount = 5) { var resourceClaims = new List(); foreach (var index in Enumerable.Range(1, resourceClaimCount)) @@ -52,9 +133,7 @@ private IReadOnlyCollection SetupResourceClaims(Application testA var resourceClaim = new ResourceClaim { ClaimName = $"TestResourceClaim{index:N}", - DisplayName = $"TestResourceClaim{index:N}", ResourceName = $"TestResourceClaim{index:N}", - Application = testApplication }; resourceClaims.Add(resourceClaim); } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetVendorsQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetVendorsQueryTests.cs index b826ad8b0..08f5911d8 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetVendorsQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetVendorsQueryTests.cs @@ -4,12 +4,13 @@ // See the LICENSE and NOTICES files in the project root for more information. using System.Collections.Generic; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using NUnit.Framework; -using Shouldly; using System.Linq; using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; @@ -40,6 +41,116 @@ public void Should_retrieve_vendors() }); } + [Test] + public void Should_retrieve_vendors_with_no_namespaces() + { + var newVendor = new Vendor + { + VendorName = "test vendor without namespace", + VendorNamespacePrefixes = null, + }; + + Save(newVendor); + + Transaction(usersContext => + { + var command = new GetVendorsQuery(usersContext, Testing.GetAppSettings()); + var allVendors = command.Execute(); + + allVendors.ShouldNotBeEmpty(); + + var vendor = allVendors.Single(v => v.VendorId == newVendor.VendorId); + vendor.VendorName.ShouldBe("test vendor without namespace"); + vendor.VendorNamespacePrefixes.ShouldBeEmpty(); + }); + } + + [Test] + public void Should_retrieve_vendors_with_filters() + { + var vendors = new Vendor[5]; + + var offset = 0; + var limit = 2; + + for (var vendorIndex = 0; vendorIndex < 5; vendorIndex++) + { + vendors[vendorIndex] = new Vendor + { + VendorName = $"test vendor {vendorIndex + 1}", + VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = $"http://testvendor{vendorIndex + 1}.net" } }, + Users = new List { new User { FullName = $"test user {vendorIndex + 1}", Email = $"testuser{vendorIndex + 1}@test.com" } } + }; + } + + Save(vendors); + + /// Id + Transaction(usersContext => + { + var command = new GetVendorsQuery(usersContext, Testing.GetAppSettings()); + + var vendorsAfterOffset = command.Execute(new CommonQueryParams(offset, limit), vendors.First().VendorId, null, null, null, null); + + vendorsAfterOffset.ShouldNotBeEmpty(); + vendorsAfterOffset.Count.ShouldBe(1); + + vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 1"); + }); + + /// Company + Transaction(usersContext => + { + var command = new GetVendorsQuery(usersContext, Testing.GetAppSettings()); + + var vendorsAfterOffset = command.Execute(new CommonQueryParams(offset, limit), null, "test vendor 2", null, null, null); + + vendorsAfterOffset.ShouldNotBeEmpty(); + vendorsAfterOffset.Count.ShouldBe(1); + + vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 2"); + }); + + /// NamespacePrefix + Transaction(usersContext => + { + var command = new GetVendorsQuery(usersContext, Testing.GetAppSettings()); + + var vendorsAfterOffset = command.Execute(new CommonQueryParams(offset, limit), null, null, "http://testvendor2.net", null, null); + + vendorsAfterOffset.ShouldNotBeEmpty(); + vendorsAfterOffset.Count.ShouldBe(1); + + vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 2"); + }); + + /// ContactName + Transaction(usersContext => + { + var command = new GetVendorsQuery(usersContext, Testing.GetAppSettings()); + + var vendorsAfterOffset = command.Execute(new CommonQueryParams(offset, limit), null, null, null, "test user 2", null); + + vendorsAfterOffset.ShouldNotBeEmpty(); + vendorsAfterOffset.Count.ShouldBe(1); + + vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 2"); + }); + + /// ContactEmailAddress + Transaction(usersContext => + { + var command = new GetVendorsQuery(usersContext, Testing.GetAppSettings()); + + var vendorsAfterOffset = command.Execute(new CommonQueryParams(offset, limit), null, null, null, null, "testuser2@test.com"); + + vendorsAfterOffset.ShouldNotBeEmpty(); + vendorsAfterOffset.Count.ShouldBe(1); + + vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 2"); + }); + } + [Test] public void Should_retrieve_vendors_with_offset_and_limit() { @@ -50,7 +161,8 @@ public void Should_retrieve_vendors_with_offset_and_limit() vendors[vendorIndex] = new Vendor { VendorName = $"test vendor {vendorIndex + 1}", - VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://testvendor.net" } } + VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://testvendor.net" } }, + Users = new List { new User { FullName = $"test user", Email = $"testuser@test.com" } } }; } @@ -59,9 +171,11 @@ public void Should_retrieve_vendors_with_offset_and_limit() Transaction(usersContext => { var command = new GetVendorsQuery(usersContext, Testing.GetAppSettings()); - var commonQueryParams = new CommonQueryParams(0, 2); - var vendorsAfterOffset = command.Execute(commonQueryParams); + var offset = 0; + var limit = 2; + + var vendorsAfterOffset = command.Execute(new CommonQueryParams(offset, limit), null, null, null, null, null); vendorsAfterOffset.ShouldNotBeEmpty(); vendorsAfterOffset.Count.ShouldBe(2); @@ -69,18 +183,18 @@ public void Should_retrieve_vendors_with_offset_and_limit() vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 1"); vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 2"); - commonQueryParams.Offset = 2; + offset = 2; - vendorsAfterOffset = command.Execute(commonQueryParams); + vendorsAfterOffset = command.Execute(new CommonQueryParams(offset, limit), null, null, null, null, null); vendorsAfterOffset.ShouldNotBeEmpty(); vendorsAfterOffset.Count.ShouldBe(2); vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 3"); vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 4"); - commonQueryParams.Offset = 4; + offset = 4; - vendorsAfterOffset = command.Execute(commonQueryParams); + vendorsAfterOffset = command.Execute(new CommonQueryParams(offset, limit), null, null, null, null, null); vendorsAfterOffset.ShouldNotBeEmpty(); vendorsAfterOffset.Count.ShouldBe(1); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/EdFi.Ods.AdminApi.DBTests.csproj b/Application/EdFi.Ods.AdminApi.DBTests/EdFi.Ods.AdminApi.DBTests.csproj index 1026f1a64..2feef5fbe 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/EdFi.Ods.AdminApi.DBTests.csproj +++ b/Application/EdFi.Ods.AdminApi.DBTests/EdFi.Ods.AdminApi.DBTests.csproj @@ -6,29 +6,36 @@ - - - - Compatability + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all - - - - - - - - + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + + + + + Always + Always - + \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi.DBTests/PlatformSecurityContextTestBase.cs b/Application/EdFi.Ods.AdminApi.DBTests/PlatformSecurityContextTestBase.cs index a140df6ce..36dfca769 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/PlatformSecurityContextTestBase.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/PlatformSecurityContextTestBase.cs @@ -7,18 +7,15 @@ using System.Threading.Tasks; using EdFi.Admin.DataAccess.Contexts; using EdFi.Security.DataAccess.Contexts; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using NUnit.Framework; using Respawn; -using Respawn.Graph; namespace EdFi.Ods.AdminApi.DBTests; [TestFixture] public abstract class PlatformSecurityContextTestBase { - private Respawner _checkpoint; - protected SqlServerSecurityContext TestContext { get; private set; } protected enum CheckpointPolicyOptions @@ -29,24 +26,21 @@ protected enum CheckpointPolicyOptions protected CheckpointPolicyOptions CheckpointPolicy { get; set; } = CheckpointPolicyOptions.BeforeEachTest; + private readonly Checkpoint _checkpoint = new() + { + TablesToIgnore = + [ + "__MigrationHistory", "DeployJournal", "AdminApiDeployJournal" + ], + SchemasToExclude = [] + }; + protected virtual string ConnectionString => TestContext.Database.GetConnectionString(); protected virtual void AdditionalFixtureSetup() { } - protected virtual async void CreateCheckpoint() - { - _checkpoint = await Respawner.CreateAsync(ConnectionString, new RespawnerOptions - { - TablesToIgnore = new Table[] - { - "__MigrationHistory", "DeployJournal", "AdminApiDeployJournal" - }, - SchemasToExclude = Array.Empty() - }); - } - protected abstract SqlServerSecurityContext CreateDbContext(); [OneTimeSetUp] @@ -56,7 +50,7 @@ public virtual async Task FixtureSetup() if (CheckpointPolicy == CheckpointPolicyOptions.BeforeAnyTest) { - await _checkpoint.ResetAsync(ConnectionString); + await _checkpoint.Reset(ConnectionString); } AdditionalFixtureSetup(); @@ -65,8 +59,12 @@ public virtual async Task FixtureSetup() [OneTimeTearDown] public async Task FixtureTearDown() { - await _checkpoint.ResetAsync(ConnectionString); - TestContext.Dispose(); + await _checkpoint.Reset(ConnectionString); + if (TestContext != null) + { + TestContext.Dispose(); + TestContext = null; + } } [SetUp] @@ -76,7 +74,7 @@ public async Task SetUp() if (CheckpointPolicy == CheckpointPolicyOptions.BeforeEachTest) { - await _checkpoint.ResetAsync(ConnectionString); + await _checkpoint.Reset(ConnectionString); } } @@ -136,5 +134,5 @@ protected TResult Transaction(Func query) return result; } - + } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/PlatformUsersContextTestBase.cs b/Application/EdFi.Ods.AdminApi.DBTests/PlatformUsersContextTestBase.cs index 80b4239cb..2c4d30a1f 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/PlatformUsersContextTestBase.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/PlatformUsersContextTestBase.cs @@ -9,7 +9,6 @@ using Microsoft.EntityFrameworkCore; using NUnit.Framework; using Respawn; -using Respawn.Graph; using static EdFi.Ods.AdminApi.DBTests.Testing; namespace EdFi.Ods.AdminApi.DBTests; @@ -17,33 +16,27 @@ namespace EdFi.Ods.AdminApi.DBTests; [TestFixture] public abstract class PlatformUsersContextTestBase { - private Respawner _checkpoint; - - protected virtual async void CreateCheckpoint() + private readonly Checkpoint _checkpoint = new() { - _checkpoint = await Respawner.CreateAsync(ConnectionString, new RespawnerOptions - { - TablesToIgnore = new Table[] - { + TablesToIgnore = + [ "__MigrationHistory", "DeployJournal", "AdminApiDeployJournal" - }, - SchemasToExclude = Array.Empty() - }); - } + ], + SchemasToExclude = [] + }; protected static string ConnectionString => AdminConnectionString; [OneTimeTearDown] public async Task FixtureTearDown() { - await _checkpoint.ResetAsync(ConnectionString); + await _checkpoint.Reset(ConnectionString); } [SetUp] public async Task SetUp() { - CreateCheckpoint(); - await _checkpoint.ResetAsync(ConnectionString); + await _checkpoint.Reset(ConnectionString); } protected static void Save(params object[] entities) @@ -51,7 +44,9 @@ protected static void Save(params object[] entities) Transaction(usersContext => { foreach (var entity in entities) + { ((SqlServerUsersContext)usersContext).Add(entity); + } }); } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/SecurityDataTestBase.cs b/Application/EdFi.Ods.AdminApi.DBTests/SecurityDataTestBase.cs index 53027f429..ee47fe2a8 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/SecurityDataTestBase.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/SecurityDataTestBase.cs @@ -7,25 +7,30 @@ using System.Collections.Generic; using System.Linq; using AutoMapper; +using EdFi.Admin.DataAccess.Contexts; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Security.DataAccess.Contexts; using EdFi.Security.DataAccess.Models; using NUnit.Framework; using Action = EdFi.Security.DataAccess.Models.Action; using ActionName = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.Action; +using Application = EdFi.Admin.DataAccess.Models.Application; using ClaimSetEditorTypes = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; - + namespace EdFi.Ods.AdminApi.DBTests; [TestFixture] public abstract class SecurityDataTestBase : PlatformSecurityContextTestBase { - protected override string ConnectionString => Testing.SecurityConnectionString; + protected override string ConnectionString => Testing.SecurityConnectionString; protected override SqlServerSecurityContext CreateDbContext() { return new SqlServerSecurityContext(Testing.GetDbContextOptions(ConnectionString)); } + public virtual string AdminTestingConnectionString => Testing.AdminConnectionString; + + public virtual SqlServerUsersContext AdminDbContext => new(Testing.GetDbContextOptions(AdminTestingConnectionString)); // This bool controls whether or not to run SecurityContext initialization // method. Setting this flag to true will cause seed data to be @@ -70,31 +75,31 @@ protected void LoadSeedData() Application GetOrCreateApplication(string applicationName) { - var application = - TestContext.Applications.FirstOrDefault(a => a.ApplicationName == applicationName); - if (application == null) - { - application = new Application - { - ApplicationName = "Ed-Fi ODS API" - }; - TestContext.Applications.Add(application); + var application = AdminDbContext.Applications.FirstOrDefault(a => a.ApplicationName == applicationName); + + if (application == null) + { + application = new Application + { + ApplicationName = "Ed-Fi ODS API" + }; + AdminDbContext.Applications.Add(application); } return application; } Action GetOrCreateAction(string actionName) { - var action = TestContext.Actions.FirstOrDefault(a => a.ActionName == actionName); - - if (action == null) - { - action = new Action - { - ActionName = actionName, - ActionUri = $"http://ed-fi.org/odsapi/actions/{actionName}" - }; - TestContext.Actions.Add(action); + var action = TestContext.Actions.FirstOrDefault(a => a.ActionName == actionName); + + if (action == null) + { + action = new Action + { + ActionName = actionName, + ActionUri = $"http://ed-fi.org/odsapi/actions/{actionName}" + }; + TestContext.Actions.Add(action); } return action; } @@ -102,21 +107,17 @@ Action GetOrCreateAction(string actionName) AuthorizationStrategy GetOrCreateAuthorizationStrategy(Application application, string displayName, string authorizationStrategyName) { - var authorizationStrategy = TestContext.AuthorizationStrategies.FirstOrDefault( - a => - a.Application.ApplicationId == application.ApplicationId && - a.DisplayName == displayName && - a.AuthorizationStrategyName == authorizationStrategyName); - - if (authorizationStrategy == null) - { - authorizationStrategy = - new AuthorizationStrategy - { - DisplayName = displayName, - AuthorizationStrategyName = authorizationStrategyName, - Application = application - }; + var authorizationStrategy = TestContext.AuthorizationStrategies.FirstOrDefault(a => + a.DisplayName == displayName && + a.AuthorizationStrategyName == authorizationStrategyName); + + if (authorizationStrategy == null) + { + authorizationStrategy = new AuthorizationStrategy + { + DisplayName = displayName, + AuthorizationStrategyName = authorizationStrategyName, + }; TestContext.AuthorizationStrategies.Add(authorizationStrategy); } @@ -126,23 +127,19 @@ AuthorizationStrategy GetOrCreateAuthorizationStrategy(Application application, ResourceClaim GetOrCreateResourceClaim(string resourceName, Application application) { var resourceClaim = - TestContext.ResourceClaims.FirstOrDefault( - r => - r.ResourceName == resourceName && - r.Application.ApplicationId == application.ApplicationId); - - if (resourceClaim == null) - { + TestContext.ResourceClaims.FirstOrDefault(r => + r.ResourceName == resourceName); + if (resourceClaim == null) + { resourceClaim = new ResourceClaim { - Application = application, - DisplayName = resourceName, ResourceName = resourceName, ClaimName = $"http://ed-fi.org/ods/identity/claims/domains/{resourceName}", ParentResourceClaim = null - }; + }; TestContext.ResourceClaims.Add(resourceClaim); } + return resourceClaim; } @@ -161,8 +158,8 @@ void GetOrCreateResourceClaimAuthorizationMetadata(Action action, { Action = action, AuthorizationStrategies = authorizationStrategy != null ? - new List { new ResourceClaimActionAuthorizationStrategies - { AuthorizationStrategy = authorizationStrategy} } : null, + [ new ResourceClaimActionAuthorizationStrategies + { AuthorizationStrategy = authorizationStrategy} ] : null, ResourceClaim = resourceClaim, ValidationRuleSetName = null }); @@ -170,7 +167,7 @@ void GetOrCreateResourceClaimAuthorizationMetadata(Action action, } } - protected IReadOnlyCollection SetupResourceClaims(Application testApplication, IList parentRcNames, IList childRcNames) + protected IReadOnlyCollection SetupResourceClaims(IList parentRcNames, IList childRcNames) { var parentResourceClaims = new List(); var childResourceClaims = new List(); @@ -180,9 +177,7 @@ protected IReadOnlyCollection SetupResourceClaims(Application tes var resourceClaim = new ResourceClaim { ClaimName = parentName, - DisplayName = parentName, ResourceName = parentName, - Application = testApplication }; parentResourceClaims.Add(resourceClaim); @@ -193,9 +188,7 @@ protected IReadOnlyCollection SetupResourceClaims(Application tes return new ResourceClaim { ClaimName = childRcName, - DisplayName = childRcName, ResourceName = childRcName, - Application = testApplication, ParentResourceClaim = resourceClaim, ParentResourceClaimId = resourceClaim.ResourceClaimId }; @@ -230,19 +223,67 @@ public static IList UniqueNameList(string prefix, int resourceClaimCount return parentResourceClaims; } - protected IReadOnlyCollection SetupParentResourceClaimsWithChildren(ClaimSet testClaimSet, Application testApplication, IList parentRcNames, IList childRcNames) + protected IReadOnlyCollection SetupClaimSetResourceClaimActions( + ClaimSet testClaimSet, + IList parentRcNames, + IList childRcNames, + IList grandChildRcNames = null + ) { var actions = ActionName.GetAll().Select(action => new Action { ActionName = action.Value, ActionUri = action.Value }).ToList(); Save(actions.Cast().ToArray()); + + var resourceClaims = SetupResourceClaimsWithChildren(parentRcNames, childRcNames, grandChildRcNames); + + var parentResourceClaims = resourceClaims.Where(rc => rc.ParentResourceClaim == null).ToList(); + var childResourceClaims = resourceClaims.Where(rc => rc.ParentResourceClaim != null && rc.ParentResourceClaim.ParentResourceClaim == null).ToList(); + var grandChildResourceClaims = resourceClaims.Where(rc => rc.ParentResourceClaim != null && rc.ParentResourceClaim.ParentResourceClaim != null).ToList(); + + + var claimSetResourceClaims = Enumerable.Range(1, parentRcNames.Count) + .Select(index => parentResourceClaims[index - 1]).Select(parentResource => new ClaimSetResourceClaimAction + { + ResourceClaim = parentResource, + Action = actions.Single(x => x.ActionName == ActionName.Create.Value), + ClaimSet = testClaimSet + }).ToList(); + + var childResources = parentResourceClaims.SelectMany(x => childResourceClaims + .Where(child => child.ParentResourceClaimId == x.ResourceClaimId) + .Select(child => new ClaimSetResourceClaimAction + { + ResourceClaim = child, + Action = actions.Single(a => a.ActionName == ActionName.Create.Value), + ClaimSet = testClaimSet + }).ToList()).ToList(); + claimSetResourceClaims.AddRange(childResources); + + var grandChildResources = grandChildResourceClaims.Select(grandChild => new ClaimSetResourceClaimAction + { + ResourceClaim = grandChild, + Action = actions.Single(a => a.ActionName == ActionName.Create.Value), + ClaimSet = testClaimSet + }).ToList(); + claimSetResourceClaims.AddRange(grandChildResources); + + Save(claimSetResourceClaims.Cast().ToArray()); + return claimSetResourceClaims; + } + + + protected IReadOnlyCollection SetupResourceClaimsWithChildren( + IList parentRcNames, + IList childRcNames, + IList grandChildRcNames = null + ) + { var parentResourceClaims = parentRcNames.Select(parentRcName => { return new ResourceClaim { ClaimName = parentRcName, - DisplayName = parentRcName, ResourceName = parentRcName, - Application = testApplication }; }).ToList(); @@ -253,39 +294,34 @@ protected IReadOnlyCollection SetupParentResourceCl return new ResourceClaim { ClaimName = childName, - DisplayName = childName, ResourceName = childName, - Application = testApplication, ParentResourceClaim = x }; - })).ToList(); - - Save(childResourceClaims.Cast().ToArray()); - - var claimSetResourceClaims = Enumerable.Range(1, parentRcNames.Count) - .Select(index => parentResourceClaims[index - 1]).Select(parentResource => new ClaimSetResourceClaimAction - { - ResourceClaim = parentResource, - Action = actions.Single(x => x.ActionName == ActionName.Create.Value), ClaimSet = testClaimSet - }).ToList(); + })).ToList(); + + var grandChildResourceClaims = grandChildRcNames == null || !grandChildRcNames.Any() ? [] : childResourceClaims.SelectMany(child => grandChildRcNames.Select(grandChildName => + { + var fullName = $"{grandChildName}-{child.ClaimName}"; + return new ResourceClaim + { + ClaimName = fullName, + ResourceName = fullName, + ParentResourceClaim = child + }; + })).ToList(); + + var allResourceClaims = parentResourceClaims + .Concat(childResourceClaims) + .Concat(grandChildResourceClaims) + .ToList(); + + Save(allResourceClaims.Cast().ToArray()); + return allResourceClaims; - var childResources = parentResourceClaims.SelectMany(x => childResourceClaims - .Where(child => child.ParentResourceClaimId == x.ResourceClaimId) - .Select(child => new ClaimSetResourceClaimAction - { - ResourceClaim = child, - Action = actions.Single(a => a.ActionName == ActionName.Create.Value), - ClaimSet = testClaimSet - }).ToList()).ToList(); - - claimSetResourceClaims.AddRange(childResources); - - Save(claimSetResourceClaims.Cast().ToArray()); - - return claimSetResourceClaims; } - - protected IReadOnlyCollection SetupApplicationAuthorizationStrategies(Application testApplication, int authStrategyCount = 5) + + + protected IReadOnlyCollection SetupApplicationAuthorizationStrategies(int authStrategyCount = 5) { var testAuthStrategies = Enumerable.Range(1, authStrategyCount) .Select(index => $"TestAuthStrategy{index}") @@ -296,7 +332,6 @@ protected IReadOnlyCollection SetupApplicationAuthorizati { AuthorizationStrategyName = x, DisplayName = x, - Application = testApplication }) .ToArray(); @@ -315,7 +350,7 @@ protected IReadOnlyCollection SetupResourcesWithDefaultAuth var rcActionAuthorizationStrategies = testAuthorizationStrategy != null ? new List { - new ResourceClaimActionAuthorizationStrategies { AuthorizationStrategy = testAuthorizationStrategy } } : null; + new() { AuthorizationStrategy = testAuthorizationStrategy } } : null; var resourceClaimWithDefaultAuthStrategy = new ResourceClaimAction { @@ -338,9 +373,8 @@ protected IReadOnlyCollection SetupResourcesWithDefaultAuth List list = null; using (var securityContext = CreateDbContext()) { - var getResourcesByClaimSetIdQuery = new ClaimSetEditorTypes.GetResourcesByClaimSetIdQuery(new StubOdsSecurityModelVersionResolver.V6(), - null, new ClaimSetEditorTypes.GetResourcesByClaimSetIdQueryV6Service(securityContext, Mapper())); - list = getResourcesByClaimSetIdQuery.AllResources(securityContextClaimSetId).ToList(); + var getResourcesByClaimSetIdQuery = new ClaimSetEditorTypes.GetResourcesByClaimSetIdQuery(securityContext, Mapper()); + list = [.. getResourcesByClaimSetIdQuery.AllResources(securityContextClaimSetId)]; } return list; } @@ -350,8 +384,7 @@ protected ClaimSetEditorTypes.ResourceClaim SingleResourceClaimForClaimSet(int s ClaimSetEditorTypes.ResourceClaim resourceClaim = null; using (var securityContext = CreateDbContext()) { - var getResourcesByClaimSetIdQuery = new ClaimSetEditorTypes.GetResourcesByClaimSetIdQuery(new StubOdsSecurityModelVersionResolver.V6(), - null, new ClaimSetEditorTypes.GetResourcesByClaimSetIdQueryV6Service(securityContext, Mapper())); + var getResourcesByClaimSetIdQuery = new ClaimSetEditorTypes.GetResourcesByClaimSetIdQuery(securityContext, Mapper()); resourceClaim = getResourcesByClaimSetIdQuery.SingleResource(securityContextClaimSetId, resourceClaimId); } return resourceClaim; diff --git a/Application/EdFi.Ods.AdminApi.DBTests/SecurityTestDatabaseSetup.cs b/Application/EdFi.Ods.AdminApi.DBTests/SecurityTestDatabaseSetup.cs deleted file mode 100644 index 1936b8b3f..000000000 --- a/Application/EdFi.Ods.AdminApi.DBTests/SecurityTestDatabaseSetup.cs +++ /dev/null @@ -1,148 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System; -using System.Data; -using System.IO.Compression; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using NuGet.Protocol; -using NuGet.Protocol.Core.Types; -using NuGet.Versioning; -using NuGet.Common; -using Microsoft.Data.SqlClient; -using Microsoft.SqlServer.Management.Common; -using Microsoft.SqlServer.Management.Smo; - -namespace EdFi.Ods.AdminApi.DBTests; - -public class SecurityTestDatabaseSetup -{ - private static SqlConnectionStringBuilder ConnectionStringBuilder - { - get - { - return new SqlConnectionStringBuilder() { ConnectionString = Testing.SecurityV53ConnectionString }; - } - } - - public static void EnsureSecurityDatabase(string downloadPath, - string version = "5.3.1146", - string nugetSource = "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_packaging/EdFi/nuget/v3/index.json", - string packageName = "EdFi.Suite3.RestApi.Databases") - { - if (!CheckSecurityDbExists()) - { - var task = Task.Run(async () => await DownloadDbPackage(packageName, version, nugetSource, downloadPath)); - var scriptsPath = task.GetAwaiter().GetResult(); - ExecuteSqlScripts(scriptsPath); - } - } - - private static async Task DownloadDbPackage(string packageName, string version, string nugetSource, string downloadPath) - { - var logger = NullLogger.Instance; - var cancellationToken = CancellationToken.None; - - var cache = new SourceCacheContext(); - var repository = Repository.Factory.GetCoreV3(nugetSource); - var resource = await repository.GetResourceAsync(); - - var packageVersion = new NuGetVersion(version); - var packagePath = Path.Combine(downloadPath, $"{packageName}.{packageVersion}.nupkg"); - if (!File.Exists(packagePath)) - { - using var packageStream = File.OpenWrite(packagePath); - - await resource.CopyNupkgToStreamAsync( - packageName, - packageVersion, - packageStream, - cache, - logger, - cancellationToken); - } - - var packageContentDir = Path.Combine(downloadPath, $"{packageName}.{packageVersion}"); - if (!Directory.Exists(packageContentDir)) - { - var result = Path.ChangeExtension(packagePath, ".zip"); - File.Move(packagePath, result); - - if (!Directory.Exists(packageContentDir)) - { - Directory.CreateDirectory(packageContentDir); - } - ZipFile.ExtractToDirectory(result, packageContentDir); - } - return packageContentDir; - } - - private static SqlConnectionStringBuilder MasterConnection - { - get - { - var csb = new SqlConnectionStringBuilder - { - ConnectionString = ConnectionStringBuilder.ConnectionString, InitialCatalog = "master" - }; - return csb; - } - } - - private static void ExecuteSqlScripts(string scriptsPath) - { - using (var connection = new SqlConnection(MasterConnection.ConnectionString)) - { - connection.Open(); - try - { - var sql = @"declare @database varchar(max) = quotename(@databaseName) - EXEC('CREATE DATABASE ' + @database + '')"; - using var command = new SqlCommand(sql, connection); - command.CommandType = CommandType.Text; - command.Parameters.AddWithValue("@databaseName", ConnectionStringBuilder.InitialCatalog); - command.ExecuteNonQuery(); - } - catch (Exception ex) - { - throw new Exception(ex.Message); - } - } - - using var conn = new SqlConnection(ConnectionStringBuilder.ConnectionString); - var server = new Server(new ServerConnection(conn)); - var scriptFilesPath = Path.Combine(scriptsPath, @"Ed-Fi-ODS\Artifacts\MsSql\Structure\Security"); - - foreach (var file in Directory.EnumerateFiles(scriptFilesPath, "*.sql")) - { - var script = File.ReadAllText(file); - server.ConnectionContext.ExecuteNonQuery(script); - } - } - - private static bool CheckSecurityDbExists() - { - try - { - var sqlCreateDBQuery = $"SELECT database_id FROM sys.databases WHERE Name = '{ConnectionStringBuilder.InitialCatalog}'"; - using var connection = new SqlConnection(MasterConnection.ConnectionString); - using var sqlCmd = new SqlCommand(sqlCreateDBQuery, connection); - connection.Open(); - var result = sqlCmd.ExecuteScalar(); - if (result != null) - { - _ = int.TryParse(result.ToString(), out var databaseID); - return databaseID > 0; - } - return false; - } - catch - { - return false; - } - } -} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/StubOdsSecurityModelVersionResolver.cs b/Application/EdFi.Ods.AdminApi.DBTests/StubOdsSecurityModelVersionResolver.cs deleted file mode 100644 index 1e5fec43c..000000000 --- a/Application/EdFi.Ods.AdminApi.DBTests/StubOdsSecurityModelVersionResolver.cs +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Ods.AdminApi.Infrastructure; - -namespace EdFi.Ods.AdminApi.DBTests; - -public static class StubOdsSecurityModelVersionResolver -{ - public class V3_5 : IOdsSecurityModelVersionResolver - { - public EdFiOdsSecurityModelCompatibility DetermineSecurityModel() - => EdFiOdsSecurityModelCompatibility.ThreeThroughFive; - } - - public class V6 : IOdsSecurityModelVersionResolver - { - public EdFiOdsSecurityModelCompatibility DetermineSecurityModel() - => EdFiOdsSecurityModelCompatibility.Six; - } -} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Testing.cs b/Application/EdFi.Ods.AdminApi.DBTests/Testing.cs index d5e8f9682..092efdc42 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Testing.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Testing.cs @@ -2,23 +2,16 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; -using EdFi.Ods.AdminApi.Helpers; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; +using EdFi.Ods.AdminApi.Common.Settings; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; namespace EdFi.Ods.AdminApi.DBTests; public static class Testing { - public static void EnsureInitialized() - { - _ = new SecurityTestDatabaseSetup(); - SecurityTestDatabaseSetup.EnsureSecurityDatabase(@"C:\\temp"); - } - private static IConfigurationRoot _config; public static IConfiguration Configuration() @@ -30,29 +23,28 @@ public static IConfiguration Configuration() return _config; } - public static string AdminConnectionString { get { return Configuration().GetConnectionString("Admin"); } } - - public static string SecurityConnectionString { get { return Configuration().GetConnectionString("Security"); } } - - public static string SecurityV53ConnectionString { get { return Configuration().GetConnectionString("SecurityV53"); } } - - public static int DefaultPageSizeOffset => (int)Configuration().GetValue(typeof(int), "AppSettings:DefaultPageSizeOffset"); - - public static int DefaultPageSizeLimit => (int)Configuration().GetValue(typeof(int), "AppSettings:DefaultPageSizeLimit"); - - public static DbContextOptions GetDbContextOptions(string connectionString) - { - var builder = new DbContextOptionsBuilder(); - builder.UseSqlServer(connectionString); - return builder.Options; - } - - public static IOptions GetAppSettings() - { - AppSettings appSettings = new AppSettings(); + public static string AdminConnectionString { get { return Configuration().GetConnectionString("EdFi_Admin"); } } + + public static string SecurityConnectionString { get { return Configuration().GetConnectionString("EdFi_Security"); } } + + public static int DefaultPageSizeOffset => Configuration().GetSection("AppSettings").GetValue("DefaultPageSizeOffset"); + + public static int DefaultPageSizeLimit => Configuration().GetSection("AppSettings").GetValue("DefaultPageSizeLimit"); + + public static DbContextOptions GetDbContextOptions(string connectionString) + { + var builder = new DbContextOptionsBuilder(); + builder.UseSqlServer(connectionString); + return builder.Options; + } + + public static IOptions GetAppSettings() + { + AppSettings appSettings = new AppSettings(); appSettings.DefaultPageSizeOffset = DefaultPageSizeOffset; appSettings.DefaultPageSizeLimit = DefaultPageSizeLimit; - IOptions options = Options.Create(appSettings); - return options; - } + IOptions options = Options.Create(appSettings); + return options; + } + } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/appsettings.json b/Application/EdFi.Ods.AdminApi.DBTests/appsettings.json index 399954041..e42caf77b 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/appsettings.json +++ b/Application/EdFi.Ods.AdminApi.DBTests/appsettings.json @@ -1,7 +1,6 @@ { "AppSettings": { "AppStartup": "OnPrem", - "ApiStartupType": "sandbox", "XsdFolder": "Schema", "DatabaseEngine": "SqlServer", "EncryptionKey": "bEnFYNociET2R1Wua3DHzwfU5u/Fa47N5fw0PXD0OSI=", @@ -9,8 +8,21 @@ "DefaultPageSizeLimit": 25 }, "ConnectionStrings": { - "Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin_Test;Integrated Security=True;Encrypt=false;Trusted_Connection=true", - "Security": "Data Source=.\\;Initial Catalog=EdFi_Security_Test;Integrated Security=True;Encrypt=false;Trusted_Connection=true", - "SecurityV53": "Data Source=.\\;Initial Catalog=EdFi_Security_Test_v53;Integrated Security=True;Encrypt=false;Trusted_Connection=true" + "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", + "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + }, + "Tenants": { + "tenant1": { + "ConnectionStrings": { + "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", + "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + } + }, + "tenant2": { + "ConnectionStrings": { + "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", + "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + } + } } } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/testsappsettings.json b/Application/EdFi.Ods.AdminApi.DBTests/testsappsettings.json new file mode 100644 index 000000000..a32758504 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/testsappsettings.json @@ -0,0 +1,20 @@ +{ + "ConnectionStrings": { + "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", + "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + }, + "Tenants": { + "tenant1": { + "ConnectionStrings": { + "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", + "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + } + }, + "tenant2": { + "ConnectionStrings": { + "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", + "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Api/OdsApiValidatorTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Api/OdsApiValidatorTests.cs index 2e340cdae..bbb932e7a 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Api/OdsApiValidatorTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Api/OdsApiValidatorTests.cs @@ -4,8 +4,7 @@ // See the LICENSE and NOTICES files in the project root for more information. using System.Threading.Tasks; -using EdFi.Ods.AdminApi.Infrastructure.Api; -using EdFi.Ods.AdminApi.Infrastructure.Services; +using EdFi.Ods.AdminApi.Common.Infrastructure.Services; using NUnit.Framework; using Shouldly; diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Api/OdsSecurityVersionResolverTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Api/OdsSecurityVersionResolverTests.cs deleted file mode 100644 index beda51292..000000000 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Api/OdsSecurityVersionResolverTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System; -using NUnit.Framework; -using Shouldly; - -namespace EdFi.Ods.AdminApi.Infrastructure.UnitTests.Api; - -[TestFixture] -public class OdsSecurityVersionResolverTests -{ - [Test] - public static void ShouldReturnV3_5ForOdsV53() => new OdsSecurityVersionResolver("5.3") - .DetermineSecurityModel().ShouldBe(EdFiOdsSecurityModelCompatibility.ThreeThroughFive); - - [Test] - public static void ShouldReturnV3_5ForOdsV53Cqe() => new OdsSecurityVersionResolver("5.3-cqe") - .DetermineSecurityModel().ShouldBe(EdFiOdsSecurityModelCompatibility.FiveThreeCqe); - - [Test] - public static void ShouldReturnV6ForOds6() => new OdsSecurityVersionResolver("6.0") - .DetermineSecurityModel().ShouldBe(EdFiOdsSecurityModelCompatibility.Six); - - [Test] - public static void ShouldReturnV6ForOdsGreaterThanV6() => new OdsSecurityVersionResolver("6.1") - .DetermineSecurityModel().ShouldBe(EdFiOdsSecurityModelCompatibility.Six); - - [Test] - public static void ShouldThrowExceptionForOdsV3() - { - Should.Throw(() => new OdsSecurityVersionResolver("3.2.0").DetermineSecurityModel()); - } - - [Test] - public static void ShouldThrowExceptionForOdsV51() - { - Should.Throw(() => new OdsSecurityVersionResolver("5.1").DetermineSecurityModel()); - } - - [Test] - public static void ShouldThrowExceptionForOdsMuchGreaterThanV6() - { - Should.Throw(() => new OdsSecurityVersionResolver("10.1").DetermineSecurityModel()); - } - - [Test] - public static void ShouldThrowExceptionWhenValidApiReturnsNoVersion() - { - Should.Throw(() => new OdsSecurityVersionResolver(string.Empty).DetermineSecurityModel()); - } -} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/EdFi.Ods.AdminApi.UnitTests.csproj b/Application/EdFi.Ods.AdminApi.UnitTests/EdFi.Ods.AdminApi.UnitTests.csproj index 3630bf31b..35363105e 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/EdFi.Ods.AdminApi.UnitTests.csproj +++ b/Application/EdFi.Ods.AdminApi.UnitTests/EdFi.Ods.AdminApi.UnitTests.csproj @@ -5,17 +5,24 @@ Copyright © 2023 Ed-Fi Alliance - - - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + - \ No newline at end of file + diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/AddApiClientOdsInstanceIdsValidationTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/AddApiClientOdsInstanceIdsValidationTests.cs new file mode 100644 index 000000000..e1d9abef6 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/AddApiClientOdsInstanceIdsValidationTests.cs @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Features.ApiClients; +using FakeItEasy; +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.ApiClients +{ + [TestFixture] + public class AddApiClientOdsInstanceIdsValidationTests + { + private IUsersContext _fakeDb; + private MethodInfo _validateMethod; + + [SetUp] + public void SetUp() + { + _fakeDb = A.Fake(); + _validateMethod = typeof(AddApiClient) + .GetMethod("ValidateOdsInstanceIds", BindingFlags.NonPublic | BindingFlags.Static); + } + + private AddApiClient.AddApiClientRequest MakeRequest(IEnumerable ids) => new AddApiClient.AddApiClientRequest + { + Name = "Test", + ApplicationId = 1, + OdsInstanceIds = ids + }; + + private void SetupOdsInstances(params int[] instanceIds) + { + var data = instanceIds.Select(id => new OdsInstance { OdsInstanceId = id }).ToList(); + var queryable = data.AsQueryable(); + + var fakeDbSet = A.Fake>(options => options.Implements(typeof(IQueryable))); + A.CallTo(() => ((IQueryable)fakeDbSet).Provider).Returns(queryable.Provider); + A.CallTo(() => ((IQueryable)fakeDbSet).Expression).Returns(queryable.Expression); + A.CallTo(() => ((IQueryable)fakeDbSet).ElementType).Returns(queryable.ElementType); + A.CallTo(() => ((IQueryable)fakeDbSet).GetEnumerator()).Returns(queryable.GetEnumerator()); + + A.CallTo(() => _fakeDb.OdsInstances).Returns(fakeDbSet); + } + + [Test] + public void Throws_When_OdsInstanceIds_NotInDatabase() + { + SetupOdsInstances(1); + var request = MakeRequest(new[] { 2, 3 }); + + var ex = Should.Throw(() => + _validateMethod.Invoke(null, new object[] { request, _fakeDb }) + ); + ex.InnerException.ShouldBeOfType(); + ex.InnerException.Message.ShouldContain("not found in database"); + } + + [Test] + public void Throws_When_OdsInstanceIds_Provided_But_None_In_Database() + { + SetupOdsInstances(); // Empty + var request = MakeRequest(new[] { 1 }); + + var ex = Should.Throw(() => + _validateMethod.Invoke(null, new object[] { request, _fakeDb }) + ); + ex.InnerException.ShouldBeOfType(); + ex.InnerException.Message.ShouldContain("not found in database"); + } + + [Test] + public void Does_Not_Throw_When_All_OdsInstanceIds_Exist() + { + SetupOdsInstances(1, 2); + var request = MakeRequest(new[] { 1, 2 }); + + Should.NotThrow(() => + _validateMethod.Invoke(null, new object[] { request, _fakeDb }) + ); + } + + [Test] + public void Does_Not_Throw_When_OdsInstanceIds_Is_Null() + { + SetupOdsInstances(1); + var request = new AddApiClient.AddApiClientRequest + { + Name = "Test", + ApplicationId = 1, + OdsInstanceIds = null + }; + + Should.NotThrow(() => + _validateMethod.Invoke(null, new object[] { request, _fakeDb }) + ); + } + + [Test] + public void Does_Not_Throw_When_OdsInstanceIds_Is_Empty() + { + SetupOdsInstances(1); + var request = MakeRequest(System.Array.Empty()); + + Should.NotThrow(() => + _validateMethod.Invoke(null, new object[] { request, _fakeDb }) + ); + } + + // Add the TearDown method to properly dispose of the _fakeDb field. + + [TearDown] + public void TearDown() + { + _fakeDb?.Dispose(); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/AddApiClientValidatorTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/AddApiClientValidatorTests.cs new file mode 100644 index 000000000..3f166a482 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/AddApiClientValidatorTests.cs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Ods.AdminApi.Features.ApiClients; +using EdFi.Ods.AdminApi.Infrastructure.Commands; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.ApiClients +{ + [TestFixture] + public class AddApiClientValidatorTests + { + private AddApiClient.Validator _validator; + + [SetUp] + public void SetUp() + { + _validator = new AddApiClient.Validator(); + } + + [Test] + public void Should_Have_Error_When_Name_Is_Empty() + { + var model = new AddApiClient.AddApiClientRequest { Name = "", ApplicationId = 1, OdsInstanceIds = new[] { 1 } }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.Name)).ShouldBeTrue(); + } + + [Test] + public void Should_Have_Error_When_Name_Exceeds_Max_Length() + { + var model = new AddApiClient.AddApiClientRequest + { + Name = new string('A', ValidationConstants.MaximumApiClientNameLength + 1), + ApplicationId = 1, + OdsInstanceIds = new[] { 1 } + }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.Name)).ShouldBeTrue(); + } + + [Test] + public void Should_Have_Error_When_ApplicationId_Is_Zero() + { + var model = new AddApiClient.AddApiClientRequest { Name = "ValidName", ApplicationId = 0, OdsInstanceIds = new[] { 1 } }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.ApplicationId)).ShouldBeTrue(); + } + + [Test] + public void Should_Have_Error_When_OdsInstanceIds_Is_Empty() + { + var model = new AddApiClient.AddApiClientRequest { Name = "ValidName", ApplicationId = 1, OdsInstanceIds = System.Array.Empty() }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.OdsInstanceIds)).ShouldBeTrue(); + } + + [Test] + public void Should_Not_Have_Error_For_Valid_Model() + { + var model = new AddApiClient.AddApiClientRequest + { + Name = "ValidName", + ApplicationId = 1, + OdsInstanceIds = new[] { 1 } + }; + var result = _validator.Validate(model); + result.IsValid.ShouldBeTrue(); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/ApiClientModelTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/ApiClientModelTests.cs new file mode 100644 index 000000000..ecbe5f8de --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/ApiClientModelTests.cs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using NUnit.Framework; +using Shouldly; +using EdFi.Ods.AdminApi.Features.ApiClients; +using System.Collections.Generic; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.ApiClients +{ + [TestFixture] + public class ApiClientModelTests + { + [Test] + public void ApiClientModel_DefaultValues_AreSetCorrectly() + { + var model = new ApiClientModel(); + model.Id.ShouldBe(0); + model.Key.ShouldBe(string.Empty); + model.Name.ShouldBe(string.Empty); + model.IsApproved.ShouldBeTrue(); + model.UseSandbox.ShouldBeFalse(); + model.SandboxType.ShouldBe(0); + model.ApplicationId.ShouldBe(0); + model.KeyStatus.ShouldBe("Active"); + model.EducationOrganizationIds.ShouldBeNull(); + model.OdsInstanceIds.ShouldBeNull(); + } + + [Test] + public void ApiClientModel_SetProperties_ValuesAreSetCorrectly() + { + var model = new ApiClientModel + { + Id = 1, + Key = "TestKey", + Name = "TestName", + IsApproved = false, + UseSandbox = true, + SandboxType = 2, + ApplicationId = 99, + KeyStatus = "Inactive", + EducationOrganizationIds = new List { 1001, 1002 }, + OdsInstanceIds = new List { 1, 2 } + }; + + model.Id.ShouldBe(1); + model.Key.ShouldBe("TestKey"); + model.Name.ShouldBe("TestName"); + model.IsApproved.ShouldBeFalse(); + model.UseSandbox.ShouldBeTrue(); + model.SandboxType.ShouldBe(2); + model.ApplicationId.ShouldBe(99); + model.KeyStatus.ShouldBe("Inactive"); + model.EducationOrganizationIds.ShouldBe(new List { 1001, 1002 }); + model.OdsInstanceIds.ShouldBe(new List { 1, 2 }); + } + } + + [TestFixture] + public class ApiClientResultTests + { + [Test] + public void ApiClientResult_DefaultValues_AreSetCorrectly() + { + var result = new ApiClientResult(); + result.Id.ShouldBe(0); + result.Name.ShouldBe(string.Empty); + result.Key.ShouldBe(string.Empty); + result.Secret.ShouldBe(string.Empty); + result.ApplicationId.ShouldBe(0); + } + + [Test] + public void ApiClientResult_SetProperties_ValuesAreSetCorrectly() + { + var result = new ApiClientResult + { + Id = 5, + Name = "ClientName", + Key = "ClientKey", + Secret = "ClientSecret", + ApplicationId = 42 + }; + + result.Id.ShouldBe(5); + result.Name.ShouldBe("ClientName"); + result.Key.ShouldBe("ClientKey"); + result.Secret.ShouldBe("ClientSecret"); + result.ApplicationId.ShouldBe(42); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/DeleteApiClientTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/DeleteApiClientTests.cs new file mode 100644 index 000000000..b3a9201a8 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/DeleteApiClientTests.cs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Threading.Tasks; +using EdFi.Ods.AdminApi.Features.ApiClients; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using FakeItEasy; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.ApiClients +{ + [TestFixture] + public class DeleteApiClientTests + { + [Test] + public async Task Handle_ExecutesDeleteCommandAndReturnsOk() + { + // Arrange + var fakeCommand = A.Fake(); + int testId = 123; + + // Act + var result = await DeleteApiClient.Handle(fakeCommand, testId); + + // Assert + A.CallTo(() => fakeCommand.Execute(testId)).MustHaveHappenedOnceExactly(); + result.ShouldNotBeNull(); + result.ShouldBeOfType>(); + } + + [Test] + public void Handle_WhenCommandThrows_ExceptionIsPropagated() + { + // Arrange + var fakeCommand = A.Fake(); + int testId = 999; + A.CallTo(() => fakeCommand.Execute(testId)).Throws(new System.Exception("Delete failed")); + + // Act & Assert + Should.Throw(async () => await DeleteApiClient.Handle(fakeCommand, testId)); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/EditApiClientOdsInstanceIdsValidationTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/EditApiClientOdsInstanceIdsValidationTests.cs new file mode 100644 index 000000000..cac6a623f --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/EditApiClientOdsInstanceIdsValidationTests.cs @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Features.ApiClients; +using FakeItEasy; +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.ApiClients +{ + [TestFixture] + public class EditApiClientOdsInstanceIdsValidationTests + { + private IUsersContext _fakeDb; + private MethodInfo _validateMethod; + + [SetUp] + public void SetUp() + { + _fakeDb = A.Fake(); + _validateMethod = typeof(EditApiClient) + .GetMethod("ValidateOdsInstanceIds", BindingFlags.NonPublic | BindingFlags.Static); + } + + private EditApiClient.EditApiClientRequest MakeRequest(IEnumerable ids) => new EditApiClient.EditApiClientRequest + { + Id = 1, + Name = "Test", + ApplicationId = 1, + OdsInstanceIds = ids + }; + + private void SetupOdsInstances(params int[] instanceIds) + { + var data = instanceIds.Select(id => new OdsInstance { OdsInstanceId = id }).ToList(); + var queryable = data.AsQueryable(); + + var fakeDbSet = A.Fake>(options => options.Implements(typeof(IQueryable))); + A.CallTo(() => ((IQueryable)fakeDbSet).Provider).Returns(queryable.Provider); + A.CallTo(() => ((IQueryable)fakeDbSet).Expression).Returns(queryable.Expression); + A.CallTo(() => ((IQueryable)fakeDbSet).ElementType).Returns(queryable.ElementType); + A.CallTo(() => ((IQueryable)fakeDbSet).GetEnumerator()).Returns(queryable.GetEnumerator()); + + A.CallTo(() => _fakeDb.OdsInstances).Returns(fakeDbSet); + } + + [Test] + public void Throws_When_OdsInstanceIds_NotInDatabase() + { + SetupOdsInstances(1); + var request = MakeRequest(new[] { 2, 3 }); + + var ex = Should.Throw(() => + _validateMethod.Invoke(null, new object[] { request, _fakeDb }) + ); + ex.InnerException.ShouldBeOfType(); + ex.InnerException.Message.ShouldContain("not found in database"); + } + + [Test] + public void Throws_When_OdsInstanceIds_Provided_But_None_In_Database() + { + SetupOdsInstances(); // Empty + var request = MakeRequest(new[] { 1 }); + + var ex = Should.Throw(() => + _validateMethod.Invoke(null, new object[] { request, _fakeDb }) + ); + ex.InnerException.ShouldBeOfType(); + ex.InnerException.Message.ShouldContain("not found in database"); + } + + [Test] + public void Does_Not_Throw_When_All_OdsInstanceIds_Exist() + { + SetupOdsInstances(1, 2); + var request = MakeRequest(new[] { 1, 2 }); + + Should.NotThrow(() => + _validateMethod.Invoke(null, new object[] { request, _fakeDb }) + ); + } + + [Test] + public void Does_Not_Throw_When_OdsInstanceIds_Is_Null() + { + SetupOdsInstances(1); + var request = new EditApiClient.EditApiClientRequest + { + Id = 1, + Name = "Test", + ApplicationId = 1, + OdsInstanceIds = null + }; + + Should.NotThrow(() => + _validateMethod.Invoke(null, new object[] { request, _fakeDb }) + ); + } + + [Test] + public void Does_Not_Throw_When_OdsInstanceIds_Is_Empty() + { + SetupOdsInstances(1); + var request = MakeRequest(System.Array.Empty()); + + Should.NotThrow(() => + _validateMethod.Invoke(null, new object[] { request, _fakeDb }) + ); + } + + [TearDown] + public void TearDown() + { + _fakeDb?.Dispose(); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/EditApiClientValidatorTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/EditApiClientValidatorTests.cs new file mode 100644 index 000000000..a5b406390 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/EditApiClientValidatorTests.cs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.Features.ApiClients; +using EdFi.Ods.AdminApi.Infrastructure.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.ApiClients +{ +#nullable enable + + public class EditApiClientModelStub : IEditApiClientModel + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public bool IsApproved { get; set; } + public int ApplicationId { get; set; } + public IEnumerable? OdsInstanceIds { get; set; } + } + + [TestFixture] + public class EditApiClientValidatorTests + { + private EditApiClient.Validator _validator; + + [SetUp] + public void SetUp() + { + _validator = new EditApiClient.Validator(); + } + + [Test] + public void Should_Have_Error_When_Id_Is_Empty() + { + var model = new EditApiClientModelStub { Id = 0, Name = "ValidName", ApplicationId = 1, OdsInstanceIds = new[] { 1 } }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.Id)).ShouldBeTrue(); + } + + [Test] + public void Should_Have_Error_When_Name_Is_Empty() + { + var model = new EditApiClientModelStub { Id = 1, Name = "", ApplicationId = 1, OdsInstanceIds = new[] { 1 } }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.Name)).ShouldBeTrue(); + } + + [Test] + public void Should_Have_Error_When_Name_Exceeds_Max_Length() + { + var model = new EditApiClientModelStub + { + Id = 1, + Name = new string('A', ValidationConstants.MaximumApiClientNameLength + 1), + ApplicationId = 1, + OdsInstanceIds = new[] { 1 } + }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.Name)).ShouldBeTrue(); + } + + [Test] + public void Should_Have_Error_When_ApplicationId_Is_Zero() + { + var model = new EditApiClientModelStub { Id = 1, Name = "ValidName", ApplicationId = 0, OdsInstanceIds = new[] { 1 } }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.ApplicationId)).ShouldBeTrue(); + } + + [Test] + public void Should_Have_Error_When_OdsInstanceIds_Is_Empty() + { + var model = new EditApiClientModelStub { Id = 1, Name = "ValidName", ApplicationId = 1, OdsInstanceIds = System.Array.Empty() }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.OdsInstanceIds)).ShouldBeTrue(); + } + + [Test] + public void Should_Not_Have_Error_For_Valid_Model() + { + var model = new EditApiClientModelStub + { + Id = 1, + Name = "ValidName", + ApplicationId = 1, + OdsInstanceIds = new[] { 1 } + }; + var result = _validator.Validate(model); + result.IsValid.ShouldBeTrue(); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/ReadApiClientTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/ReadApiClientTests.cs new file mode 100644 index 000000000..638e757ed --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/ReadApiClientTests.cs @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoMapper; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Features.ApiClients; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using FakeItEasy; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.ApiClients +{ + [TestFixture] + public class ReadApiClientTests + { + [Test] + public async Task GetApiClients_ReturnsOkWithMappedList() + { + // Arrange + var fakeQuery = A.Fake(); + var fakeMapper = A.Fake(); + int appId = 42; + var queryResult = new List { new ApiClient() }; + var mappedResult = new List { new ApiClientModel { Id = 1 } }; + A.CallTo(() => fakeQuery.Execute(appId)).Returns(queryResult); + A.CallTo(() => fakeMapper.Map>(queryResult)).Returns(mappedResult); + + // Act + var result = await ReadApiClient.GetApiClients(fakeQuery, fakeMapper, appId); + + // Assert + result.ShouldBeOfType>>(); + var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; + okResult!.Value.ShouldBe(mappedResult); + } + + [Test] + public async Task GetApiClient_ReturnsOkWithMappedModel() + { + // Arrange + var fakeQuery = A.Fake(); + var fakeMapper = A.Fake(); + int id = 7; + var queryResult = new ApiClient(); + var mappedModel = new ApiClientModel { Id = id }; + A.CallTo(() => fakeQuery.Execute(id)).Returns(queryResult); + A.CallTo(() => fakeMapper.Map(queryResult)).Returns(mappedModel); + + // Act + var result = await ReadApiClient.GetApiClient(fakeQuery, fakeMapper, id); + + // Assert + result.ShouldBeOfType>(); + var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok; + okResult!.Value.ShouldBe(mappedModel); + } + + [Test] + public void GetApiClient_WhenNotFound_ThrowsNotFoundException() + { + // Arrange + var fakeQuery = A.Fake(); + var fakeMapper = A.Fake(); + int id = 99; + A.CallTo(() => fakeQuery.Execute(id)).Returns(null); + + // Act & Assert + Should.Throw>(() => ReadApiClient.GetApiClient(fakeQuery, fakeMapper, id).GetAwaiter().GetResult()); + } + + [Test] + public void GetApiClients_WhenQueryThrows_ExceptionIsPropagated() + { + // Arrange + var fakeQuery = A.Fake(); + var fakeMapper = A.Fake(); + int appId = 42; + A.CallTo(() => fakeQuery.Execute(appId)).Throws(new System.Exception("Query failed")); + + // Act & Assert + Should.Throw(async () => await ReadApiClient.GetApiClients(fakeQuery, fakeMapper, appId)); + } + + [Test] + public void GetApiClient_WhenMapperThrows_ExceptionIsPropagated() + { + // Arrange + var fakeQuery = A.Fake(); + var fakeMapper = A.Fake(); + int id = 7; + var queryResult = new ApiClient(); + A.CallTo(() => fakeQuery.Execute(id)).Returns((ApiClient)queryResult); + A.CallTo(() => fakeMapper.Map(queryResult)).Throws(new System.Exception("Mapping failed")); + + // Act & Assert + Should.Throw(async () => await ReadApiClient.GetApiClient(fakeQuery, fakeMapper, id)); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/ResetApiClientCredentialsTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/ResetApiClientCredentialsTests.cs new file mode 100644 index 000000000..8a417936a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/ApiClients/ResetApiClientCredentialsTests.cs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Threading.Tasks; +using AutoMapper; +using EdFi.Ods.AdminApi.Features.ApiClients; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using FakeItEasy; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.ApiClients +{ + [TestFixture] + public class ResetApiClientCredentialsTests + { + [Test] + public async Task HandleResetCredentials_ExecutesCommandAndReturnsOk() + { + // Arrange + var fakeCommand = A.Fake(); + var fakeMapper = A.Fake(); + int id = 123; + var commandResult = new RegenerateApiClientSecretResult(); + var mappedResult = new ApiClientResult { Id = id, Key = "key", Secret = "secret" }; + A.CallTo(() => fakeCommand.Execute(id)).Returns(commandResult); + A.CallTo(() => fakeMapper.Map(commandResult)).Returns(mappedResult); + + // Act + var result = await ResetApiClientCredentials.HandleResetCredentials(fakeCommand, fakeMapper, id); + + // Assert + result.ShouldBeOfType>(); + var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok; + okResult!.Value.ShouldBe(mappedResult); + } + + [Test] + public void HandleResetCredentials_WhenCommandThrows_ExceptionIsPropagated() + { + // Arrange + var fakeCommand = A.Fake(); + var fakeMapper = A.Fake(); + int id = 999; + A.CallTo(() => fakeCommand.Execute(id)).Throws(new System.Exception("Reset failed")); + + // Act & Assert + Should.Throw(async () => await ResetApiClientCredentials.HandleResetCredentials(fakeCommand, fakeMapper, id)); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/ReadTenantsTest.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/ReadTenantsTest.cs new file mode 100644 index 000000000..6a5a9b086 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/ReadTenantsTest.cs @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using EdFi.Common.Configuration; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Features.Tenants; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.Tenants; + +[TestFixture] +public class ReadTenantsTest +{ + [Test] + public async Task GetTenantsByTenantIdAsync_ReturnsOk_WhenTenantExists() + { + var tenantsService = A.Fake(); + var memoryCache = A.Fake(); + var options = A.Fake>(); + var tenantName = "tenant1"; + + var tenant = new TenantModel + { + TenantName = tenantName, + ConnectionStrings = new TenantModelConnectionStrings + { + EdFiAdminConnectionString = "Host=adminhost;Database=admindb;", + EdFiSecurityConnectionString = "Host=sechost;Database=secdb;" + } + }; + + A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres" }); + A.CallTo(() => tenantsService.GetTenantByTenantIdAsync(tenantName)).Returns(tenant); + + var result = await ReadTenants.GetTenantsByTenantIdAsync(tenantsService, memoryCache, tenantName, options); + + result.ShouldNotBeNull(); + var okResult = result as IResult; + okResult.ShouldNotBeNull(); + } + + [Test] + public async Task GetTenantsByTenantIdAsync_ReturnsNotFound_WhenTenantDoesNotExist() + { + var tenantsService = A.Fake(); + var memoryCache = A.Fake(); + var options = A.Fake>(); + var tenantName = "missingTenant"; + + A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres" }); + A.CallTo(() => tenantsService.GetTenantByTenantIdAsync(tenantName)).Returns((TenantModel)null); + + var result = await ReadTenants.GetTenantsByTenantIdAsync(tenantsService, memoryCache, tenantName, options); + + result.ShouldNotBeNull(); + var notFoundResult = result as IResult; + notFoundResult.ShouldNotBeNull(); + } + + [Test] + public async Task GetTenantsAsync_ReturnsOk_WithTenantList() + { + var tenantsService = A.Fake(); + var memoryCache = A.Fake(); + var options = A.Fake>(); + var databaseEngine = DatabaseEngine.Postgres; + + var tenants = new List + { + new() { + TenantName = "tenant1", + ConnectionStrings = new TenantModelConnectionStrings + { + EdFiAdminConnectionString = "Host=adminhost;Database=admindb;", + EdFiSecurityConnectionString = "Host=sechost;Database=secdb;" + } + } + }; + + A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres" }); + A.CallTo(() => tenantsService.GetTenantsAsync(true)).Returns(Task.FromResult(tenants)); + + var result = await ReadTenants.GetTenantsAsync(tenantsService, memoryCache, options); + + result.ShouldNotBeNull(); + var okResult = result as IResult; + okResult.ShouldNotBeNull(); + } + + [Test] + public void GetTenantsByTenantIdAsync_ThrowsNotFoundException_WhenDatabaseEngineIsNull() + { + var tenantsService = A.Fake(); + var memoryCache = A.Fake(); + var options = A.Fake>(); + var tenantName = "tenant1"; + + A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = null }); + + Should.ThrowAsync>(async () => + { + await ReadTenants.GetTenantsByTenantIdAsync(tenantsService, memoryCache, tenantName, options); + }); + } + + [Test] + public void GetTenantsAsync_ThrowsNotFoundException_WhenDatabaseEngineIsNull() + { + var tenantsService = A.Fake(); + var memoryCache = A.Fake(); + var options = A.Fake>(); + + A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = null }); + + Should.ThrowAsync>(async () => + { + await ReadTenants.GetTenantsAsync(tenantsService, memoryCache, options); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/TenantModelTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/TenantModelTests.cs new file mode 100644 index 000000000..eb0e940fe --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/TenantModelTests.cs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Features.Tenants; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.Tenants; + +[TestFixture] +public class TenantModelTests +{ + [Test] + public void DefaultConstructor_ShouldInitializePropertiesToEmptyStrings() + { + // Act + var connectionStrings = new TenantModelConnectionStrings(); + + // Assert + connectionStrings.EdFiAdminConnectionString.ShouldBe(string.Empty); + connectionStrings.EdFiSecurityConnectionString.ShouldBe(string.Empty); + } + + [Test] + public void ParameterizedConstructor_ShouldSetProperties() + { + // Arrange + var adminConn = "AdminConn"; + var securityConn = "SecurityConn"; + + // Act + var connectionStrings = new TenantModelConnectionStrings(adminConn, securityConn); + + // Assert + connectionStrings.EdFiAdminConnectionString.ShouldBe(adminConn); + connectionStrings.EdFiSecurityConnectionString.ShouldBe(securityConn); + } + + [Test] + public void Properties_ShouldBeSettable() + { + // Arrange + var connectionStrings = new TenantModelConnectionStrings + { + // Act + EdFiAdminConnectionString = "NewAdmin", + EdFiSecurityConnectionString = "NewSecurity" + }; + + // Assert + connectionStrings.EdFiAdminConnectionString.ShouldBe("NewAdmin"); + connectionStrings.EdFiSecurityConnectionString.ShouldBe("NewSecurity"); + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/HealthCheckServiceExtensionsTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/HealthCheckServiceExtensionsTests.cs new file mode 100644 index 000000000..3d6b56908 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/HealthCheckServiceExtensionsTests.cs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using Shouldly; +using System.Collections.Generic; +using System.Linq; + +namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure; + +[TestFixture] +public class HealthCheckServiceExtensionsTests +{ + [Test] + public void AddHealthCheck_ShouldRegisterBothAdminAndSecurityHealthChecks_WhenMultiTenancyDisabled() + { + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); // Required for health checks + var configuration = CreateTestConfiguration(multiTenancy: false); + + // Act + services.AddHealthCheck(configuration); + + // Assert - Check that health check services are registered + var healthCheckServiceDescriptor = services.FirstOrDefault(s => s.ServiceType == typeof(HealthCheckService)); + healthCheckServiceDescriptor.ShouldNotBeNull(); + } + + [Test] + public void AddHealthCheck_ShouldRegisterMultiTenantHealthChecks_WhenMultiTenancyEnabled() + { + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); // Required for health checks + var configuration = CreateTestConfiguration(multiTenancy: true); + + // Act + services.AddHealthCheck(configuration); + + // Assert - Check that health check services are registered + var healthCheckServiceDescriptor = services.FirstOrDefault(s => s.ServiceType == typeof(HealthCheckService)); + healthCheckServiceDescriptor.ShouldNotBeNull(); + } + + private static IConfigurationRoot CreateTestConfiguration(bool multiTenancy) + { + var configData = new Dictionary + { + ["AppSettings:DatabaseEngine"] = "SqlServer", + ["AppSettings:MultiTenancy"] = multiTenancy.ToString(), + ["ConnectionStrings:EdFi_Admin"] = "Data Source=test;Initial Catalog=EdFi_Admin_Test;Integrated Security=True", + ["ConnectionStrings:EdFi_Security"] = "Data Source=test;Initial Catalog=EdFi_Security_Test;Integrated Security=True" + }; + + if (multiTenancy) + { + configData["Tenants:tenant1:ConnectionStrings:EdFi_Admin"] = "Data Source=test;Initial Catalog=EdFi_Admin_Tenant1;Integrated Security=True"; + configData["Tenants:tenant1:ConnectionStrings:EdFi_Security"] = "Data Source=test;Initial Catalog=EdFi_Security_Tenant1;Integrated Security=True"; + configData["Tenants:tenant2:ConnectionStrings:EdFi_Admin"] = "Data Source=test;Initial Catalog=EdFi_Admin_Tenant2;Integrated Security=True"; + configData["Tenants:tenant2:ConnectionStrings:EdFi_Security"] = "Data Source=test;Initial Catalog=EdFi_Security_Tenant2;Integrated Security=True"; + } + + return new ConfigurationBuilder() + .AddInMemoryCollection(configData) + .Build(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs new file mode 100644 index 000000000..d83fc139c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Features.Tenants; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; +using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Services.Tenants; + +[TestFixture] +internal class TenantServiceTests +{ + private IOptionsSnapshot _options = null!; + private IMemoryCache _memoryCache = null!; + private AppSettingsFile _appSettings = null!; + + [SetUp] + public void SetUp() + { + _options = A.Fake>(); + _memoryCache = new MemoryCache(new MemoryCacheOptions()); + _appSettings = new AppSettingsFile + { + AppSettings = new AppSettings + { + MultiTenancy = true, + DatabaseEngine = "SqlServer" + }, + Tenants = new Dictionary + { + { + "tenantA", new TenantSettings + { + ConnectionStrings = new Dictionary + { + { "EdFi_Admin", "admin-conn-A" }, + { "EdFi_Security", "security-conn-A" } + } + } + }, + { + "tenantB", new TenantSettings + { + ConnectionStrings = new Dictionary + { + { "EdFi_Admin", "admin-conn-B" }, + { "EdFi_Security", "security-conn-B" } + } + } + } + }, + ConnectionStrings = new Dictionary + { + { "EdFi_Admin", "admin-conn-default" }, + { "EdFi_Security", "security-conn-default" } + }, + SwaggerSettings = new(), + Testing = new() + }; + + A.CallTo(() => _options.Value).Returns(_appSettings); + } + + [TearDown] + public void TearDown() + { + _memoryCache.Dispose(); + } + + [Test] + public async Task GetTenantsAsync_Should_Return_All_Tenants_When_MultiTenancy_Enabled() + { + var service = new TenantService(_options, _memoryCache); + + var tenants = await service.GetTenantsAsync(); + + tenants.Count.ShouldBe(2); + tenants.Any(t => t.TenantName == "tenantA").ShouldBeTrue(); + tenants.Any(t => t.TenantName == "tenantB").ShouldBeTrue(); + } + + [Test] + public async Task GetTenantsAsync_Should_Return_DefaultTenant_When_MultiTenancy_Disabled() + { + _appSettings.AppSettings.MultiTenancy = false; + var service = new TenantService(_options, _memoryCache); + + var tenants = await service.GetTenantsAsync(); + + tenants.Count.ShouldBe(1); + tenants[0].TenantName.ShouldBe(Constants.DefaultTenantName); + tenants[0].ConnectionStrings.EdFiAdminConnectionString.ShouldBe("admin-conn-default"); + tenants[0].ConnectionStrings.EdFiSecurityConnectionString.ShouldBe("security-conn-default"); + } + + [Test] + public async Task GetTenantByTenantIdAsync_Should_Return_Correct_Tenant() + { + var service = new TenantService(_options, _memoryCache); + + var tenant = await service.GetTenantByTenantIdAsync("tenantA"); + + tenant.ShouldNotBeNull(); + tenant!.TenantName.ShouldBe("tenantA"); + tenant.ConnectionStrings.EdFiAdminConnectionString.ShouldBe("admin-conn-A"); + tenant.ConnectionStrings.EdFiSecurityConnectionString.ShouldBe("security-conn-A"); + } + + [Test] + public async Task GetTenantByTenantIdAsync_Should_Return_Null_If_Not_Found() + { + var service = new TenantService(_options, _memoryCache); + + var tenant = await service.GetTenantByTenantIdAsync("notfound"); + + tenant.ShouldBeNull(); + } + + [Test] + public async Task InitializeTenantsAsync_Should_Store_Tenants_In_Cache() + { + var service = new TenantService(_options, _memoryCache); + + await service.InitializeTenantsAsync(); + + var cached = _memoryCache.Get>(Constants.TenantsCacheKey); + cached.ShouldNotBeNull(); + cached!.Count.ShouldBe(2); + } + + [Test] + public async Task GetTenantsAsync_Should_Return_From_Cache_If_Requested() + { + var service = new TenantService(_options, _memoryCache); + + // Prime the cache + await service.InitializeTenantsAsync(); + + // Remove a tenant from the underlying settings to prove cache is used + _appSettings.Tenants.Remove("tenantA"); + + var tenants = await service.GetTenantsAsync(fromCache: true); + + tenants.Count.ShouldBe(2); + tenants.Any(t => t.TenantName == "tenantA").ShouldBeTrue(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/WebApplicationBuilderExtensionsTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/WebApplicationBuilderExtensionsTests.cs new file mode 100644 index 000000000..4c78fbfd6 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/WebApplicationBuilderExtensionsTests.cs @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Net; +using System.Threading.RateLimiting; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure; + +[TestFixture] +public class WebApplicationBuilderExtensionsTests +{ + [Test] + public void ConfigureRateLimiting_WhenDisabled_ShouldConfigureNoLimiter() + { + // Arrange + var builder = CreateWebApplicationBuilder(false); + + // Act + WebApplicationBuilderExtensions.ConfigureRateLimiting(builder); + + // Assert + var services = builder.Services.BuildServiceProvider(); + var options = services.GetRequiredService>().Value; + + options.ShouldNotBeNull(); + + // Build an app to test the global limiter + var app = builder.Build(); + var httpContext = new DefaultHttpContext(); + var globalLimiter = app.Services.GetRequiredService>().Value.GlobalLimiter; + + globalLimiter.ShouldNotBeNull(); + + // The global limiter should be configured as a NoLimiter when disabled + var acquireResult = globalLimiter.AttemptAcquire(httpContext); + acquireResult.IsAcquired.ShouldBeTrue(); + } + + [Test] + public void ConfigureRateLimiting_WhenEnabled_ShouldConfigureRejectionStatusCode() + { + // Arrange + var builder = CreateWebApplicationBuilder(true, httpStatusCode: 429); + + // Act + WebApplicationBuilderExtensions.ConfigureRateLimiting(builder); + + // Assert + var services = builder.Services.BuildServiceProvider(); + var options = services.GetRequiredService>().Value; + + options.ShouldNotBeNull(); + options.RejectionStatusCode.ShouldBe(429); + } + + [Test] + public void ConfigureRateLimiting_WithEndpointRules_ShouldConfigureEndpointSpecificLimiters() + { + // Arrange + var builder = CreateWebApplicationBuilder(true, new[] + { + new GeneralRule { Endpoint = "POST:/Connect/Register", Period = "1m", Limit = 3 } + }); + + // Act + WebApplicationBuilderExtensions.ConfigureRateLimiting(builder); + + // Assert + var services = builder.Services.BuildServiceProvider(); + var options = services.GetRequiredService>().Value; + + options.ShouldNotBeNull(); + + // Build an app to test the endpoint limiters + var app = builder.Build(); + var globalLimiter = app.Services.GetRequiredService>().Value.GlobalLimiter; + + globalLimiter.ShouldNotBeNull(); + + // Test with a matching endpoint + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "POST"; + httpContext.Request.Path = "/Connect/Register"; + + // First 3 requests should be allowed for the matching endpoint + for (int i = 0; i < 3; i++) + { + var acquireResult = globalLimiter.AttemptAcquire(httpContext); + acquireResult.IsAcquired.ShouldBeTrue($"Request {i + 1} should be allowed"); + } + + // 4th request should be rejected + var fourthResult = globalLimiter.AttemptAcquire(httpContext); + fourthResult.IsAcquired.ShouldBeFalse("4th request should be rejected"); + + // Test with a non-matching endpoint + var differentContext = new DefaultHttpContext(); + differentContext.Request.Method = "GET"; + differentContext.Request.Path = "/api/different"; + + var differentResult = globalLimiter.AttemptAcquire(differentContext); + differentResult.IsAcquired.ShouldBeTrue("Non-matching endpoint should be allowed"); + } + + private static WebApplicationBuilder CreateWebApplicationBuilder( + bool enableEndpointRateLimiting, + IEnumerable generalRules = null, + int httpStatusCode = (int)HttpStatusCode.TooManyRequests) + { + var configValues = new Dictionary + { + { "IpRateLimiting:EnableEndpointRateLimiting", enableEndpointRateLimiting.ToString() }, + { "IpRateLimiting:HttpStatusCode", httpStatusCode.ToString() } + }; + + if (generalRules != null) + { + int index = 0; + foreach (var rule in generalRules) + { + configValues[$"IpRateLimiting:GeneralRules:{index}:Endpoint"] = rule.Endpoint; + configValues[$"IpRateLimiting:GeneralRules:{index}:Period"] = rule.Period; + configValues[$"IpRateLimiting:GeneralRules:{index}:Limit"] = rule.Limit.ToString(); + index++; + } + } + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configValues) + .Build(); + + var builder = WebApplication.CreateBuilder(); + builder.Configuration.AddConfiguration(configuration); + return builder; + } + + private class GeneralRule + { + public string Endpoint { get; set; } = string.Empty; + public string Period { get; set; } = string.Empty; + public int Limit { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/AssertionExtensions.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/AssertionExtensions.cs new file mode 100644 index 000000000..03cd379da --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/AssertionExtensions.cs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Results; +using Shouldly; +using static System.Environment; + +namespace EdFi.Ods.AdminApi.V1.DBTests; + +public static class AssertionExtensions +{ + public static void ShouldValidate(this AbstractValidator validator, TModel model) + => validator.Validate(model).ShouldBeSuccessful(); + + public static void ShouldNotValidate(this AbstractValidator validator, TModel model, params string[] expectedErrors) + => validator.Validate(model).ShouldBeFailure(expectedErrors); + + private static void ShouldBeSuccessful(this ValidationResult result) + { + var indentedErrorMessages = result + .Errors + .OrderBy(x => x.ErrorMessage) + .Select(x => " " + x.ErrorMessage) + .ToArray(); + + var actual = string.Join(NewLine, indentedErrorMessages); + + result.IsValid.ShouldBeTrue($"Expected no validation errors, but found {result.Errors.Count}:{NewLine}{actual}"); + } + + private static void ShouldBeFailure(this ValidationResult result, params string[] expectedErrors) + { + result.IsValid.ShouldBeFalse("Expected validation errors, but the message passed validation."); + + result.Errors + .OrderBy(x => x.ErrorMessage) + .Select(x => x.ErrorMessage) + .ToArray() + .ShouldBe([.. expectedErrors.OrderBy(x => x)]); + } + + public static void ShouldSatisfy(this IEnumerable actual, params Action[] itemExpectations) + { + var actualItems = actual.ToArray(); + + if (actualItems.Length != itemExpectations.Length) + throw new Exception( + $"Expected the collection to have {itemExpectations.Length} " + + $"items, but there were {actualItems.Length} items."); + + for (var i = 0; i < actualItems.Length; i++) + { + try + { + itemExpectations[i](actualItems[i]); + } + catch (Exception failure) + { + throw new Exception($"Assertion failed for item at position [{i}].", failure); + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/AddClaimSetCommandServiceTests.cs similarity index 56% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandV53ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/AddClaimSetCommandServiceTests.cs index ab17834dc..96b24678f 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/AddClaimSetCommandV53ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/AddClaimSetCommandServiceTests.cs @@ -2,20 +2,19 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; using System; using System.Linq; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; using NUnit.Framework; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; using Shouldly; -using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet; -using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application; +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; +using ClaimSet = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ClaimSet; -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.DBTests.ClaimSetEditorTests; [TestFixture] -public class AddClaimSetCommandV53ServiceTests : SecurityData53TestBase +public class AddClaimSetCommandServiceTests : SecurityDataTestBase { [Test] public void ShouldAddClaimSet() @@ -30,12 +29,14 @@ public void ShouldAddClaimSet() var addedClaimSetId = 0; ClaimSet addedClaimSet = null; - using (var context = base.TestContext) + using (var securityContext = TestContext) { - var command = new AddClaimSetCommandV53Service(context); + var command = new AddClaimSetCommandService(securityContext); addedClaimSetId = command.Execute(newClaimSet); - addedClaimSet = context.ClaimSets.Single(x => x.ClaimSetId == addedClaimSetId); + addedClaimSet = securityContext.ClaimSets.Single(x => x.ClaimSetId == addedClaimSetId); } addedClaimSet.ClaimSetName.ShouldBe(newClaimSet.ClaimSetName); + addedClaimSet.ForApplicationUseOnly.ShouldBe(false); + addedClaimSet.IsEdfiPreset.ShouldBe(false); } } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandV6ServiceTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandServiceTests.cs similarity index 93% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandV6ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandServiceTests.cs index 8df77a01c..719ea3fd8 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandV6ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/DeleteClaimSetCommandServiceTests.cs @@ -5,18 +5,18 @@ using System; using System.Linq; -using NUnit.Framework; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; using Moq; +using NUnit.Framework; using Shouldly; -using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; -using Application = EdFi.Security.DataAccess.Models.Application; +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; +using ClaimSet = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ClaimSet; -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.DBTests.ClaimSetEditorTests; [TestFixture] -public class DeleteClaimSetCommandV6ServiceTests : SecurityDataTestBase +public class DeleteClaimSetCommandServiceTests : SecurityDataTestBase { [Test] @@ -44,7 +44,7 @@ public void ShouldDeleteClaimSet() deleteModel.Setup(x => x.Id).Returns(testClaimSetToDelete.ClaimSetId); using var securityContext = TestContext; - var command = new DeleteClaimSetCommandV6Service(securityContext); + var command = new DeleteClaimSetCommandService(securityContext); command.Execute(deleteModel.Object); var deletedClaimSet = securityContext.ClaimSets.SingleOrDefault(x => x.ClaimSetId == testClaimSetToDelete.ClaimSetId); deletedClaimSet.ShouldBeNull(); @@ -93,7 +93,7 @@ public void ShouldThrowExceptionOnEditSystemReservedClaimSet() using var securityContext = TestContext; var exception = Assert.Throws(() => { - var command = new DeleteClaimSetCommandV6Service(securityContext); + var command = new DeleteClaimSetCommandService(securityContext); command.Execute(deleteModel.Object); }); exception.ShouldNotBeNull(); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandV6ServiceTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/EditClaimSetCommandServiceTests.cs similarity index 90% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandV6ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/EditClaimSetCommandServiceTests.cs index 67e5c5e43..015d30710 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditClaimSetCommandV6ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/EditClaimSetCommandServiceTests.cs @@ -5,21 +5,21 @@ using System; using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Features.ClaimSets; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; using NUnit.Framework; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; using Shouldly; -using EdFi.Security.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using VendorApplication = EdFi.Admin.DataAccess.Models.Application; -using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; -using Application = EdFi.Security.DataAccess.Models.Application; -using EdFi.Ods.AdminApi.Features.ClaimSets; -using EdFi.Ods.AdminApi.Infrastructure; +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; +using ClaimSet = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ClaimSet; +using VendorApplication = EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models.Application; -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.DBTests.ClaimSetEditorTests; [TestFixture] -public class EditClaimSetCommandV6ServiceTests : SecurityDataTestBase +public class EditClaimSetCommandServiceTests : SecurityDataTestBase { [Test] public void ShouldEditClaimSet() @@ -38,7 +38,7 @@ public void ShouldEditClaimSet() using var securityContext = TestContext; UsersTransaction((usersContext) => { - var command = new EditClaimSetCommandV6Service(securityContext, usersContext); + var command = new EditClaimSetCommandService(securityContext, usersContext); command.Execute(editModel); }); @@ -63,7 +63,7 @@ public void ShouldThrowExceptionOnEditSystemReservedClaimSet() using var securityContext = TestContext; var exception = Assert.Throws(() => UsersTransaction(usersContext => { - var command = new EditClaimSetCommandV6Service(TestContext, usersContext); + var command = new EditClaimSetCommandService(TestContext, usersContext); command.Execute(editModel); })); exception.ShouldNotBeNull(); @@ -92,7 +92,7 @@ public void ShouldEditClaimSetWithVendorApplications() using var securityContext = TestContext; UsersTransaction(usersContext => { - var command = new EditClaimSetCommandV6Service(securityContext, usersContext); + var command = new EditClaimSetCommandService(securityContext, usersContext); command.Execute(editModel); }); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandV6ServiceTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandServiceTests.cs similarity index 93% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandV6ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandServiceTests.cs index 45364bf09..82f0d7c1e 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandV6ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/EditResourceOnClaimSetCommandServiceTests.cs @@ -5,18 +5,18 @@ using System; using System.Linq; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using Moq; using NUnit.Framework; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; using Shouldly; -using Moq; -using Application = EdFi.Security.DataAccess.Models.Application; -using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; -using ResourceClaim = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaim; +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; +using ClaimSet = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ClaimSet; +using ResourceClaim = EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor.ResourceClaim; -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.DBTests.ClaimSetEditorTests; [TestFixture] -public class EditResourceOnClaimSetCommandV6ServiceTests : SecurityDataTestBase +public class EditResourceOnClaimSetCommandServiceTests : SecurityDataTestBase { [Test] public void ShouldEditParentResourcesOnClaimSet() @@ -43,7 +43,7 @@ public void ShouldEditParentResourcesOnClaimSet() Create = false, Read = false, Update = true, - Delete = true, + Delete = true, ReadChanges = true }; @@ -52,7 +52,7 @@ public void ShouldEditParentResourcesOnClaimSet() editResourceOnClaimSetModel.Setup(x => x.ResourceClaim).Returns(editedResource); using var securityContext = TestContext; - var command = new EditResourceOnClaimSetCommandV6Service(securityContext); + var command = new EditResourceOnClaimSetCommandService(securityContext); command.Execute(editResourceOnClaimSetModel.Object); var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); @@ -105,7 +105,7 @@ public void ShouldEditChildResourcesOnClaimSet() Create = false, Read = false, Update = true, - Delete = true, + Delete = true, ReadChanges = true }; @@ -113,7 +113,7 @@ public void ShouldEditChildResourcesOnClaimSet() editResourceOnClaimSetModel.Setup(x => x.ClaimSetId).Returns(testClaimSet.ClaimSetId); editResourceOnClaimSetModel.Setup(x => x.ResourceClaim).Returns(editedResource); - var command = new EditResourceOnClaimSetCommandV6Service(securityContext); + var command = new EditResourceOnClaimSetCommandService(securityContext); command.Execute(editResourceOnClaimSetModel.Object); var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); @@ -167,7 +167,7 @@ public void ShouldAddParentResourceToClaimSet() Create = true, Read = false, Update = true, - Delete = false, + Delete = false, ReadChanges = false }; var existingResources = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); @@ -179,7 +179,7 @@ public void ShouldAddParentResourceToClaimSet() }; using var securityContext = TestContext; - var command = new EditResourceOnClaimSetCommandV6Service(securityContext); + var command = new EditResourceOnClaimSetCommandService(securityContext); command.Execute(editResourceOnClaimSetModel); var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); @@ -221,7 +221,7 @@ public void ShouldAddChildResourcesToClaimSet() Create = true, Read = false, Update = true, - Delete = false, + Delete = false, ReadChanges = false }; var existingResources = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); @@ -232,7 +232,7 @@ public void ShouldAddChildResourcesToClaimSet() ResourceClaim = resourceToAdd }; - var command = new EditResourceOnClaimSetCommandV6Service(securityContext); + var command = new EditResourceOnClaimSetCommandService(securityContext); command.Execute(editResourceOnClaimSetModel); var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetAllAuthorizationStrategiesQueryTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetAllAuthorizationStrategiesQueryTests.cs new file mode 100644 index 000000000..81ebbb1ce --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetAllAuthorizationStrategiesQueryTests.cs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V1.DBTests.ClaimSetEditorTests; + +[TestFixture] +public class GetAllAuthorizationStrategiesQueryTests : SecurityDataTestBase +{ + [Test] + public void ShouldGetAllAuthorizationStrategies() + { + LoadSeedData(); + + using var securityContext = TestContext; + var query = new GetAllAuthorizationStrategiesQuery(securityContext); + var resultNames = query.Execute().Select(x => x.AuthStrategyName).ToList(); + + resultNames.Count.ShouldBe(2); + + resultNames.ShouldContain("NamespaceBased"); + resultNames.ShouldContain("NoFurtherAuthorizationRequired"); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetAllClaimSetsQueryV6ServiceTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetAllClaimSetsQueryServiceTests.cs similarity index 70% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetAllClaimSetsQueryV6ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetAllClaimSetsQueryServiceTests.cs index 259bab1af..0558dc61b 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetAllClaimSetsQueryV6ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetAllClaimSetsQueryServiceTests.cs @@ -5,19 +5,19 @@ using System; using System.Linq; -using EdFi.Ods.AdminApi.Infrastructure; -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; -using EdFi.Security.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; using NUnit.Framework; using Shouldly; -using ClaimSetModel = EdFi.Security.DataAccess.Models.ClaimSet; +using ClaimSetModel = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ClaimSet; -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.DBTests.ClaimSetEditorTests; [TestFixture] -public class GetAllClaimSetsQueryV6ServiceTests : SecurityDataTestBase +public class GetAllClaimSetsQueryServiceTests : SecurityDataTestBase { - public GetAllClaimSetsQueryV6ServiceTests() + public GetAllClaimSetsQueryServiceTests() { SeedSecurityContextOnFixtureSetup = true; } @@ -25,7 +25,7 @@ public GetAllClaimSetsQueryV6ServiceTests() [Test] public void Should_Retrieve_ClaimSetNames() { - var application = new Application + var application = new Security.DataAccess.Models.Application { ApplicationName = $"Test Application {DateTime.Now:O}" }; @@ -37,7 +37,7 @@ public void Should_Retrieve_ClaimSetNames() var claimSetNames = Transaction(securityContext => { - var query = new GetAllClaimSetsQueryV6Service(securityContext, Testing.GetAppSettings()); + var query = new GetAllClaimSetsQueryService(securityContext, Testing.GetAppSettings()); return query.Execute().Select(x => x.Name).ToArray(); }); @@ -48,7 +48,7 @@ public void Should_Retrieve_ClaimSetNames() [Test] public void Should_Retrieve_EdfiPreset_ClaimSet() { - var application = new Application + var application = new Security.DataAccess.Models.Application { ApplicationName = $"Test Application {DateTime.Now:O}" }; @@ -60,18 +60,18 @@ public void Should_Retrieve_EdfiPreset_ClaimSet() var claimSets = Transaction(securityContext => { - var query = new GetAllClaimSetsQueryV6Service(securityContext, Testing.GetAppSettings()); + var query = new GetAllClaimSetsQueryService(securityContext, Testing.GetAppSettings()); return query.Execute().ToArray(); }); - var edfiPresetClaimSet = claimSets.Where( x=> x.Name.Equals(claimSet1.ClaimSetName) && x.IsEditable == false).ToList(); - edfiPresetClaimSet.Count().ShouldBe(1); + var edfiPresetClaimSet = claimSets.Where(x => x.Name.Equals(claimSet1.ClaimSetName) && x.IsEditable == false).ToList(); + edfiPresetClaimSet.Count.ShouldBe(1); } [Test] public void Should_Retrieve_ClaimSetNames_with_offset_and_limit() { - var application = new Application + var application = new Security.DataAccess.Models.Application { ApplicationName = $"Test Application {DateTime.Now:O}" }; @@ -84,11 +84,11 @@ public void Should_Retrieve_ClaimSetNames_with_offset_and_limit() var claimSet5 = GetClaimSet(application); Save(claimSet1, claimSet2, claimSet3, claimSet4, claimSet5); - + var commonQueryParams = new CommonQueryParams(0, 2); var claimSetNames = Transaction(securityContext => { - var query = new GetAllClaimSetsQueryV6Service(securityContext, Testing.GetAppSettings()); + var query = new GetAllClaimSetsQueryService(securityContext, Testing.GetAppSettings()); return query.Execute(commonQueryParams).Select(x => x.Name).ToArray(); }); @@ -99,7 +99,7 @@ public void Should_Retrieve_ClaimSetNames_with_offset_and_limit() commonQueryParams.Offset = 2; claimSetNames = Transaction(securityContext => { - var query = new GetAllClaimSetsQueryV6Service(securityContext, Testing.GetAppSettings()); + var query = new GetAllClaimSetsQueryService(securityContext, Testing.GetAppSettings()); return query.Execute(commonQueryParams).Select(x => x.Name).ToArray(); }); @@ -110,7 +110,7 @@ public void Should_Retrieve_ClaimSetNames_with_offset_and_limit() commonQueryParams.Offset = 4; claimSetNames = Transaction(securityContext => { - var query = new GetAllClaimSetsQueryV6Service(securityContext, Testing.GetAppSettings()); + var query = new GetAllClaimSetsQueryService(securityContext, Testing.GetAppSettings()); return query.Execute(commonQueryParams).Select(x => x.Name).ToArray(); }); @@ -119,13 +119,13 @@ public void Should_Retrieve_ClaimSetNames_with_offset_and_limit() } private static int _claimSetId = 0; - private static ClaimSetModel GetClaimSet(Application application, bool IsEdfiPreset = false) + private static ClaimSetModel GetClaimSet(Security.DataAccess.Models.Application application, bool IsEdfiPreset = false) { return new ClaimSetModel { Application = application, ClaimSetName = $"Test Claim Set {_claimSetId++} - {DateTime.Now:O}", - IsEdfiPreset= IsEdfiPreset + IsEdfiPreset = IsEdfiPreset }; } } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryV53ServiceTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryServiceTests.cs similarity index 78% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryV53ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryServiceTests.cs index 9a4f76254..77a37dfac 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryV53ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetIdQueryServiceTests.cs @@ -2,23 +2,22 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; using System; using System.Collections.Generic; using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; using NUnit.Framework; using Shouldly; -using Application = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Application; -using ClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet; -using VendorApplication = EdFi.Admin.DataAccess.Models.Application; -using ClaimSetEditor = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure; +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; +using ClaimSet = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ClaimSet; +using VendorApplication = EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models.Application; -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.DBTests.ClaimSetEditorTests; [TestFixture] -public class GetApplicationsByClaimSetIdQueryV53ServiceTests : SecurityData53TestBase +public class GetApplicationsByClaimSetIdQueryServiceTests : SecurityDataTestBase { [TestCase(1)] [TestCase(3)] @@ -28,17 +27,14 @@ public void ShouldGetApplicationsByClaimSetId(int applicationCount) var testClaimSets = SetupApplicationWithClaimSets(); SetupApplications(testClaimSets, applicationCount); - using var securityContext = TestContext; foreach (var testClaimSet in testClaimSets) { - List results = null; - UsersTransaction(usersContext => { - var query = new GetApplicationsByClaimSetId53Query(securityContext, usersContext); - results = query.Execute(testClaimSet.ClaimSetId).ToList(); + var query = new GetApplicationsByClaimSetIdQuery(securityContext, usersContext); + var results = query.Execute(testClaimSet.ClaimSetId).ToList(); var testApplications = usersContext.Applications.Where(x => x.ClaimSetName == testClaimSet.ClaimSetName).Select(x => new Application @@ -60,15 +56,16 @@ public void ShouldGetClaimSetApplicationsCount(int applicationsCount) var testClaimSets = SetupApplicationWithClaimSets(); SetupApplications(testClaimSets, applicationsCount); + using var securityContext = TestContext; foreach (var testClaimSet in testClaimSets) { UsersTransaction(usersContext => { - var query = new GetApplicationsByClaimSetId53Query(securityContext, usersContext); + var query = new GetApplicationsByClaimSetIdQuery(securityContext, usersContext); var appsCountByClaimSet = query.ExecuteCount(testClaimSet.ClaimSetId); var testApplicationsCount = - usersContext.Applications.Count(x => x.ClaimSetName == testClaimSet.ClaimSetName); + usersContext.Applications.Count(x => x.ClaimSetName == testClaimSet.ClaimSetName); appsCountByClaimSet.ShouldBe(testApplicationsCount); }); } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetAuthStrategiesByApplicationNameQueryTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetAuthStrategiesByApplicationNameQueryTests.cs similarity index 91% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetAuthStrategiesByApplicationNameQueryTests.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetAuthStrategiesByApplicationNameQueryTests.cs index 27d9b57fe..04f3cc28c 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetAuthStrategiesByApplicationNameQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetAuthStrategiesByApplicationNameQueryTests.cs @@ -4,13 +4,12 @@ // See the LICENSE and NOTICES files in the project root for more information. using System.Linq; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; using NUnit.Framework; using Shouldly; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; -using Application = EdFi.Security.DataAccess.Models.Application; - -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.DBTests.ClaimSetEditorTests; [TestFixture] public class GetAuthStrategiesByApplicationNameQueryTests : SecurityDataTestBase diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryV6ServiceTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryServiceTests.cs similarity index 82% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryV6ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryServiceTests.cs index 2ff5bcdf8..1eecd32d4 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryV6ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetClaimSetByIdQueryServiceTests.cs @@ -4,20 +4,19 @@ // See the LICENSE and NOTICES files in the project root for more information. using System; +using System.Net; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; using NUnit.Framework; using Shouldly; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using System.Net; -using EdFi.Security.DataAccess.Contexts; - -using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; -using Application = EdFi.Security.DataAccess.Models.Application; +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; +using ClaimSet = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ClaimSet; -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.DBTests.ClaimSetEditorTests; [TestFixture] -public class GetClaimSetByIdQueryV6ServiceTests : SecurityDataTestBase +public class GetClaimSetByIdQueryServiceTests : SecurityDataTestBase { [Test] public void ShouldGetClaimSetById() @@ -38,7 +37,7 @@ public void ShouldGetClaimSetById() Save(testClaimSet); using var securityContext = TestContext; - var query = new GetClaimSetByIdQueryV6Service(securityContext); + var query = new GetClaimSetByIdQueryService(securityContext); var result = query.Execute(testClaimSet.ClaimSetId); result.Name.ShouldBe(testClaimSet.ClaimSetName); result.Id.ShouldBe(testClaimSet.ClaimSetId); @@ -72,7 +71,7 @@ public void ShouldGetNonEditableClaimSetById() Save(edfiPresetClaimSet); using var securityContext = TestContext; - var query = new GetClaimSetByIdQueryV6Service(securityContext); + var query = new GetClaimSetByIdQueryService(securityContext); var result = query.Execute(systemReservedClaimSet.ClaimSetId); result.Name.ShouldBe(systemReservedClaimSet.ClaimSetName); result.Id.ShouldBe(systemReservedClaimSet.ClaimSetId); @@ -95,7 +94,7 @@ public void ShouldThrowExceptionForNonExistingClaimSetId() var adminApiException = Assert.Throws(() => { - var query = new GetClaimSetByIdQueryV6Service(securityContext); + var query = new GetClaimSetByIdQueryService(securityContext); query.Execute(NonExistingClaimSetId); }); adminApiException.ShouldNotBeNull(); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryV6ServiceTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryServiceTests.cs similarity index 92% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryV6ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryServiceTests.cs index c75033636..8d95ee64a 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryV6ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/GetResourcesByClaimSetIdQueryServiceTests.cs @@ -5,16 +5,16 @@ using System.Collections.Generic; using System.Linq; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; using NUnit.Framework; using Shouldly; -using Application = EdFi.Security.DataAccess.Models.Application; -using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; -using ResourceClaim = EdFi.Security.DataAccess.Models.ResourceClaim; -using Action = EdFi.Security.DataAccess.Models.Action; -using ActionName = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.Action; -using EdFi.Security.DataAccess.Models; +using Action = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Action; +using ActionName = EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor.Action; +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; +using ClaimSet = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ClaimSet; +using ResourceClaim = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ResourceClaim; -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.DBTests.ClaimSetEditorTests; [TestFixture] public class GetResourcesByClaimSetIdQueryV6SerivceTests : SecurityDataTestBase @@ -129,8 +129,8 @@ public void ShouldGetDefaultAuthorizationStrategiesForParentResourcesByClaimSetI Save(testClaimSet); var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies(testApplication).ToList(); - var testResourceClaims = SetupParentResourceClaims(new List { testClaimSet }, testApplication, UniqueNameList("ParentRc", 3)); - var testAuthStrategies = SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, testResourceClaims.ToList()); + var testResourceClaims = SetupParentResourceClaims([testClaimSet], testApplication, UniqueNameList("ParentRc", 3)); + var testAuthStrategies = SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, [.. testResourceClaims]); var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId).ToArray(); results.Select(x => x.DefaultAuthStrategiesForCRUD[0].AuthorizationStrategies[0].AuthStrategyName).ShouldBe(testAuthStrategies.Select(x => x.AuthorizationStrategies.Single() @@ -156,8 +156,8 @@ public void ShouldGetDefaultAuthorizationStrategiesForSingleResourcesByClaimSetI var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies(testApplication).ToList(); var rcIds = UniqueNameList("Parent", 1); - var testResourceClaims = SetupParentResourceClaims(new List { testClaimSet }, testApplication, rcIds); - var testAuthStrategies = SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, testResourceClaims.ToList()); + var testResourceClaims = SetupParentResourceClaims([testClaimSet], testApplication, rcIds); + var testAuthStrategies = SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, [.. testResourceClaims]); var rcName = $"{rcIds.First()}{testClaimSet.ClaimSetName}"; var testResourceClaim = @@ -197,8 +197,8 @@ public void ShouldGetDefaultAuthorizationStrategiesForParentResourcesWithChildre var appAuthorizationStrategies = SetupApplicationAuthorizationStrategies(testApplication).ToList(); - var testResourceClaims = SetupParentResourceClaimsWithChildren(new List { testClaimSet }, testApplication); - var testAuthStrategies = SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, testResourceClaims.ToList()); + var testResourceClaims = SetupParentResourceClaimsWithChildren([testClaimSet], testApplication); + var testAuthStrategies = SetupResourcesWithDefaultAuthorizationStrategies(appAuthorizationStrategies, [.. testResourceClaims]); var results = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId).ToArray(); @@ -265,7 +265,9 @@ private IReadOnlyCollection SetupParentResourceClai }; var claimSetResourceClaim = new ClaimSetResourceClaimAction { - ResourceClaim = resourceClaim, Action = action, ClaimSet = claimSet + ResourceClaim = resourceClaim, + Action = action, + ClaimSet = claimSet }; claimSetResourceClaims.Add(claimSetResourceClaim); } @@ -329,7 +331,7 @@ private IReadOnlyCollection SetupParentResourceClai Save(claimSetResourceClaims.Cast().ToArray()); - claimSetResourceClaims = new List(); + claimSetResourceClaims = []; foreach (var claimSet in claimSets) { foreach (var index in Enumerable.Range(1, resourceClaimCount)) diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/OverrideDefaultAuthorizationStrategyV6ServiceTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/OverrideDefaultAuthorizationStrategyServiceTests.cs similarity index 73% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/OverrideDefaultAuthorizationStrategyV6ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/OverrideDefaultAuthorizationStrategyServiceTests.cs index 82060aa4f..ca5b2a748 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/OverrideDefaultAuthorizationStrategyV6ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/OverrideDefaultAuthorizationStrategyServiceTests.cs @@ -3,18 +3,18 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using System.Collections.Generic; using System.Linq; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; using NUnit.Framework; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; using Shouldly; -using System.Collections.Generic; -using Application = EdFi.Security.DataAccess.Models.Application; -using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; +using ClaimSet = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ClaimSet; -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.DBTests.ClaimSetEditorTests; [TestFixture] -public class OverrideDefaultAuthorizationStrategyV6ServiceTests : SecurityDataTestBase +public class OverrideDefaultAuthorizationStrategyServiceTests : SecurityDataTestBase { [Test] public void ShouldOverrideAuthorizationStrategiesForParentResourcesOnClaimSet() @@ -41,7 +41,7 @@ public void ShouldOverrideAuthorizationStrategiesForParentResourcesOnClaimSet() testClaimSet, testApplication, parentRcNames, UniqueNameList("Child", 1)); SetupResourcesWithDefaultAuthorizationStrategies( - appAuthorizationStrategies, testResourceClaims.ToList()); + appAuthorizationStrategies, [.. testResourceClaims]); var testResource1ToEdit = testResourceClaims.Select(x => x.ResourceClaim) .Single(x => x.ResourceName == parentRcNames.First()); @@ -53,22 +53,21 @@ public void ShouldOverrideAuthorizationStrategiesForParentResourcesOnClaimSet() { ResourceClaimId = testResource1ToEdit.ResourceClaimId, ClaimSetId = testClaimSet.ClaimSetId, - AuthorizationStrategyForCreate = new int[1] { appAuthorizationStrategies - .Single(x => x.AuthorizationStrategyName == "TestAuthStrategy4").AuthorizationStrategyId }, - AuthorizationStrategyForRead = new int[0], - AuthorizationStrategyForUpdate = new int[0], - AuthorizationStrategyForDelete = new int[0], - AuthorizationStrategyForReadChanges = new int[0], + AuthorizationStrategyForCreate = [ appAuthorizationStrategies + .Single(x => x.AuthorizationStrategyName == "TestAuthStrategy4").AuthorizationStrategyId ], + AuthorizationStrategyForRead = [], + AuthorizationStrategyForUpdate = [], + AuthorizationStrategyForDelete = [], + AuthorizationStrategyForReadChanges = [], }; List resourceClaimsForClaimSet = null; using var securityContext = TestContext; - var command = new OverrideDefaultAuthorizationStrategyV6Service(securityContext); + var command = new OverrideDefaultAuthorizationStrategyService(securityContext); command.Execute(overrideModel); - var getResourcesByClaimSetIdQuery = new GetResourcesByClaimSetIdQuery(new StubOdsSecurityModelVersionResolver.V6(), - null, new GetResourcesByClaimSetIdQueryV6Service(securityContext, SecurityDataTestBase.Mapper())); - resourceClaimsForClaimSet = getResourcesByClaimSetIdQuery.AllResources(testClaimSet.ClaimSetId).ToList(); + var getResourcesByClaimSetIdQuery = new GetResourcesByClaimSetIdQuery(new GetResourcesByClaimSetIdQueryService(securityContext, SecurityDataTestBase.Mapper())); + resourceClaimsForClaimSet = [.. getResourcesByClaimSetIdQuery.AllResources(testClaimSet.ClaimSetId)]; var resultResourceClaim1 = resourceClaimsForClaimSet.Single(x => x.Id == overrideModel.ResourceClaimId); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/UpdateResourcesOnClaimSetCommandV6ServiceTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/UpdateResourcesOnClaimSetCommandServiceTests.cs similarity index 78% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/UpdateResourcesOnClaimSetCommandV6ServiceTests.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/UpdateResourcesOnClaimSetCommandServiceTests.cs index 686b9573f..cf825b30e 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/UpdateResourcesOnClaimSetCommandV6ServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/ClaimSetEditorTests/UpdateResourcesOnClaimSetCommandServiceTests.cs @@ -4,21 +4,20 @@ // See the LICENSE and NOTICES files in the project root for more information. using System; +using System.Collections.Generic; using System.Linq; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using Moq; using NUnit.Framework; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; using Shouldly; -using System.Collections.Generic; -using Moq; - -using Application = EdFi.Security.DataAccess.Models.Application; -using ClaimSet = EdFi.Security.DataAccess.Models.ClaimSet; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; +using ClaimSet = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ClaimSet; -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.DBTests.ClaimSetEditorTests; [TestFixture] -public class UpdateResourcesOnClaimSetCommandV6ServiceTests : SecurityDataTestBase +public class UpdateResourcesOnClaimSetCommandServiceTests : SecurityDataTestBase { [Test] public void ShouldUpdateResourcesOnClaimSet() @@ -35,7 +34,7 @@ public void ShouldUpdateResourcesOnClaimSet() var parentRcNames = UniqueNameList("ParentRc", 2); var childName = "ChildRc098"; var testResources = SetupParentResourceClaimsWithChildren(testClaimSet, testApplication, parentRcNames, - new List { childName }); + [childName]); var testParentResource = testResources.Single(x => x.ResourceClaim.ResourceName == parentRcNames.First()); var secondTestParentResource = testResources.Single(x => x.ResourceClaim.ResourceName == parentRcNames.Last()); @@ -45,7 +44,7 @@ public void ShouldUpdateResourcesOnClaimSet() var testChildResource1ToEdit = securityContext.ResourceClaims.Single(x => x.ResourceName == firstParentChildName && x.ParentResourceClaimId == testParentResource.ResourceClaim.ResourceClaimId); var addedResourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); - addedResourceClaimsForClaimSet.Count().ShouldBe(2); + addedResourceClaimsForClaimSet.Count.ShouldBe(2); var secondParentResourceClaim = addedResourceClaimsForClaimSet.Single(x => x.Id == secondTestParentResource.ResourceClaim.ResourceClaimId); secondParentResourceClaim.ShouldNotBeNull(); @@ -58,16 +57,16 @@ public void ShouldUpdateResourcesOnClaimSet() Update = true, Delete = true, ReadChanges = true, - Children = new List {new ResourceClaim + Children = [new ResourceClaim { Id = testChildResource1ToEdit.ResourceClaimId, Name = testChildResource1ToEdit.ResourceName, Create = false, Read = false, Update = true, - Delete = true, + Delete = true, ReadChanges = true, - } } + } ] }; var updatedResourceClaims = new List @@ -81,14 +80,12 @@ public void ShouldUpdateResourcesOnClaimSet() using var securityContext6 = CreateDbContext(); var addOrEditResourcesOnClaimSetCommand = new AddOrEditResourcesOnClaimSetCommand( - new EditResourceOnClaimSetCommand(new StubOdsSecurityModelVersionResolver.V6(), - null, new EditResourceOnClaimSetCommandV6Service(securityContext6)), + new EditResourceOnClaimSetCommand(new EditResourceOnClaimSetCommandService(securityContext6)), new GetResourceClaimsQuery(securityContext6), new OverrideDefaultAuthorizationStrategyCommand( - new StubOdsSecurityModelVersionResolver.V6(), null, - new OverrideDefaultAuthorizationStrategyV6Service(securityContext6))); + new OverrideDefaultAuthorizationStrategyService(securityContext6))); - var command = new UpdateResourcesOnClaimSetCommandV6Service(securityContext6, addOrEditResourcesOnClaimSetCommand); + var command = new UpdateResourcesOnClaimSetCommandService(securityContext6, addOrEditResourcesOnClaimSetCommand); command.Execute(updateResourcesOnClaimSetModel.Object); var resourceClaimsForClaimSet = ResourceClaimsForClaimSet(testClaimSet.ClaimSetId); diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/AddApplicationCommandTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/AddApplicationCommandTests.cs new file mode 100644 index 000000000..0a3e3ca85 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/AddApplicationCommandTests.cs @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.CommandTests; + +[TestFixture] +public class AddApplicationCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldFailForInvalidVendor() + { + // Fix for CS9035: Ensure the required 'Vendor' property is set in the object initializer. + var vendor = new Vendor + { + VendorNamespacePrefixes = + [ + new() { + NamespacePrefix = "http://tests.com", + Vendor = new Vendor() // Set the required 'Vendor' property + } + ], + VendorName = "Integration Tests" + }; + + Save(vendor); + + Transaction(usersContext => + { + var command = new AddApplicationCommand(usersContext, new InstanceContext()); + var newApplication = new TestApplication + { + ApplicationName = "Production-Test Application", + ClaimSetName = "FakeClaimSet", + ProfileId = 0, + VendorId = 0 + }; + + Assert.Throws(() => command.Execute(newApplication)); + }); + } + + [Test] + public void ProfileShouldBeOptional() + { + var vendor = new Vendor + { + VendorNamespacePrefixes = + [ + new VendorNamespacePrefix + { + NamespacePrefix = "http://tests.com", + Vendor = new Vendor() // Set the required 'Vendor' property + } + ], + VendorName = "Integration Tests" + }; + + var odsInstance = new OdsInstance + { + Name = "test ods instance", + InstanceType = "test type", + Status = "test status", + IsExtended = true, + Version = "test version" + }; + + Save(vendor, odsInstance); + + AddApplicationResult result = null; + + Transaction(usersContext => + { + var command = new AddApplicationCommand(usersContext, new InstanceContext()); + var newApplication = new TestApplication + { + ApplicationName = "Test Application", + ClaimSetName = "FakeClaimSet", + ProfileId = null, + OdsInstanceId = odsInstance.OdsInstanceId, + VendorId = vendor.VendorId, + EducationOrganizationIds = [12345, 67890] + }; + + result = command.Execute(newApplication); + }); + + Transaction(usersContext => + { + var persistedApplication = usersContext.Applications + .Include(x => x.Profiles) + .Include(x => x.ApplicationEducationOrganizations) + .Include(x => x.Vendor) + .Include(x => x.ApiClients) + .Include(x => x.OdsInstance) + .Single(a => a.ApplicationId == result.ApplicationId); + + persistedApplication.ClaimSetName.ShouldBe("FakeClaimSet"); + persistedApplication.Profiles.Count.ShouldBe(0); + persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(2); + persistedApplication.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890).ShouldBeTrue(); + + persistedApplication.Vendor.VendorId.ShouldBeGreaterThan(0); + persistedApplication.Vendor.VendorId.ShouldBe(vendor.VendorId); + + persistedApplication.OdsInstance.OdsInstanceId.ShouldBe(odsInstance.OdsInstanceId); + + persistedApplication.ApiClients.Count.ShouldBe(1); + var apiClient = persistedApplication.ApiClients.First(); + apiClient.Name.ShouldBe("Test Application"); + apiClient.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890).ShouldBeTrue(); + apiClient.Key.ShouldBe(result.Key); + apiClient.Secret.ShouldBe(result.Secret); + }); + } + + [Test] + public void OdsInstanceShouldBeOptional() + { + var vendor = new Vendor + { + VendorNamespacePrefixes = + [ + new VendorNamespacePrefix + { + NamespacePrefix = "http://tests.com", + Vendor = new Vendor() // Fix: Set the required 'Vendor' property + } + ], + VendorName = "Integration Tests" + }; + + Save(vendor); + + AddApplicationResult result = null; + + Transaction(usersContext => + { + var command = new AddApplicationCommand(usersContext, new InstanceContext()); + var newApplication = new TestApplication + { + ApplicationName = "Test Application", + ClaimSetName = "FakeClaimSet", + ProfileId = null, + OdsInstanceId = null, + VendorId = vendor.VendorId, + EducationOrganizationIds = [12345, 67890] + }; + + result = command.Execute(newApplication); + }); + + Transaction(usersContext => + { + var persistedApplication = usersContext.Applications + .Include(x => x.Profiles) + .Include(x => x.OdsInstance) + .Include(x => x.ApplicationEducationOrganizations) + .Include(x => x.Vendor) + .Include(x => x.ApiClients) + .Single(a => a.ApplicationId == result.ApplicationId); + + persistedApplication.ClaimSetName.ShouldBe("FakeClaimSet"); + persistedApplication.OdsInstance.ShouldBeNull(); + persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(2); + persistedApplication.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890).ShouldBeTrue(); + + persistedApplication.Vendor.VendorId.ShouldBeGreaterThan(0); + persistedApplication.Vendor.VendorId.ShouldBe(vendor.VendorId); + + persistedApplication.ApiClients.Count.ShouldBe(1); + var apiClient = persistedApplication.ApiClients.First(); + apiClient.Name.ShouldBe("Test Application"); + apiClient.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890).ShouldBeTrue(); + apiClient.Key.ShouldBe(result.Key); + apiClient.Secret.ShouldBe(result.Secret); + }); + } + + [Test] + public void ShouldExecute() + { + const string OdsInstanceName = "Test Instance"; + var vendor = new Vendor + { + VendorNamespacePrefixes = + [ + new VendorNamespacePrefix + { + NamespacePrefix = "http://tests.com", + Vendor = new Vendor { VendorName = "Integration Tests" } + } + ], + VendorName = "Integration Tests" + }; + + var profile = new Profile + { + ProfileName = "Test Profile" + }; + + var odsInstance = new OdsInstance + { + Name = OdsInstanceName, + InstanceType = "Ods", + IsExtended = false, + Status = "OK", + Version = "1.0.0" + }; + + Save(vendor, profile, odsInstance); + + var instanceContext = new InstanceContext + { + Id = 1, + Name = OdsInstanceName + }; + + AddApplicationResult result = null; + Transaction(usersContext => + { + var command = new AddApplicationCommand(usersContext, instanceContext); + var newApplication = new TestApplication + { + ApplicationName = "Test Application", + ClaimSetName = "FakeClaimSet", + ProfileId = profile.ProfileId, + OdsInstanceId = odsInstance.OdsInstanceId, + VendorId = vendor.VendorId, + EducationOrganizationIds = [12345, 67890] + }; + + result = command.Execute(newApplication); + }); + + Transaction(usersContext => + { + var persistedApplication = usersContext.Applications + .Include(x => x.Profiles) + .Include(x => x.ApplicationEducationOrganizations) + .Include(x => x.Vendor) + .Include(x => x.ApiClients) + .Include(x => x.OdsInstance) + .Single(a => a.ApplicationId == result.ApplicationId); + + persistedApplication.ClaimSetName.ShouldBe("FakeClaimSet"); + persistedApplication.Profiles.Count.ShouldBe(1); + persistedApplication.Profiles.First().ProfileName.ShouldBe("Test Profile"); + persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(2); + persistedApplication.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890).ShouldBeTrue(); + + persistedApplication.Vendor.VendorId.ShouldBeGreaterThan(0); + persistedApplication.Vendor.VendorId.ShouldBe(vendor.VendorId); + + persistedApplication.ApiClients.Count.ShouldBe(1); + var apiClient = persistedApplication.ApiClients.First(); + apiClient.Name.ShouldBe("Test Application"); + apiClient.ApplicationEducationOrganizations.All(o => o.EducationOrganizationId == 12345 || o.EducationOrganizationId == 67890).ShouldBeTrue(); + apiClient.Key.ShouldBe(result.Key); + apiClient.Secret.ShouldBe(result.Secret); + + persistedApplication.OdsInstance.ShouldNotBeNull(); + persistedApplication.OdsInstance.Name.ShouldBe(OdsInstanceName); + }); + } + + private class TestApplication : IAddApplicationModel + { + public string ApplicationName { get; set; } + public int VendorId { get; set; } + public string ClaimSetName { get; set; } + public int? ProfileId { get; set; } + public int? OdsInstanceId { get; set; } + public IEnumerable EducationOrganizationIds { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/AddOdsInstanceCommandTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/AddOdsInstanceCommandTests.cs new file mode 100644 index 000000000..123b8f277 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/AddOdsInstanceCommandTests.cs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using Moq; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.CommandTests; + +public class AddOdsInstanceCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldAddOdsInstance() + { + var newOdsInstance = new Mock(); + newOdsInstance.Setup(x => x.Name).Returns("test ods instance"); + newOdsInstance.Setup(x => x.InstanceType).Returns("test type"); + newOdsInstance.Setup(x => x.Status).Returns("test status"); + newOdsInstance.Setup(x => x.IsExtended).Returns(true); + newOdsInstance.Setup(x => x.Version).Returns("test version"); + + var id = 0; + Transaction(usersContext => + { + var command = new AddOdsInstanceCommand(usersContext); + + id = command.Execute(newOdsInstance.Object).OdsInstanceId; + id.ShouldBeGreaterThan(0); + }); + + Transaction(usersContext => + { + var odsInstance = usersContext.OdsInstances + .Single(v => v.OdsInstanceId == id); + odsInstance.Name.ShouldBe("test ods instance"); + odsInstance.InstanceType.ShouldBe("test type"); + odsInstance.Status.ShouldBe("test status"); + odsInstance.IsExtended.ShouldBe(true); + odsInstance.Version.ShouldBe("test version"); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/AddVendorCommandTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/AddVendorCommandTests.cs new file mode 100644 index 000000000..b99170fe7 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/AddVendorCommandTests.cs @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.V1.Infrastructure.Helpers; +using Microsoft.EntityFrameworkCore; +using Moq; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.CommandTests; + +[TestFixture] +public class AddVendorCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldAddVendor() + { + var newVendor = new Mock(); + newVendor.Setup(x => x.Company).Returns("test vendor"); + newVendor.Setup(x => x.NamespacePrefixes).Returns("http://www.test.com/"); + newVendor.Setup(x => x.ContactName).Returns("test user"); + newVendor.Setup(x => x.ContactEmailAddress).Returns("test@test.com"); + + var id = 0; + Transaction(usersContext => + { + var command = new AddVendorCommand(usersContext); + + id = command.Execute(newVendor.Object).VendorId; + id.ShouldBeGreaterThan(0); + }); + + Transaction(usersContext => + { + var vendor = usersContext.Vendors + .Include(x => x.VendorNamespacePrefixes) + .Include(x => x.Users) + .Single(v => v.VendorId == id); + vendor.VendorName.ShouldBe("test vendor"); + vendor.VendorNamespacePrefixes.First().NamespacePrefix.ShouldBe("http://www.test.com/"); + vendor.Users.Single().FullName.ShouldBe("test user"); + vendor.Users.Single().Email.ShouldBe("test@test.com"); + }); + } + + [Test] + public void ShouldAddVendorIfMultipleNamespacePrefixes() + { + var newVendor = new Mock(); + var namespacePrefixes = new List + { + "http://www.test1.com/", + "http://www.test2.com/", + "http://www.test3.com/" + }; + newVendor.Setup(x => x.Company).Returns("test vendor"); + newVendor.Setup(x => x.NamespacePrefixes).Returns(namespacePrefixes.ToDelimiterSeparated()); + newVendor.Setup(x => x.ContactName).Returns("test user"); + newVendor.Setup(x => x.ContactEmailAddress).Returns("test@test.com"); + + var id = 0; + Transaction(usersContext => + { + var command = new AddVendorCommand(usersContext); + + id = command.Execute(newVendor.Object).VendorId; + id.ShouldBeGreaterThan(0); + }); + + Transaction(usersContext => + { + var vendor = usersContext.Vendors + .Include(x => x.VendorNamespacePrefixes) + .Include(x => x.Users).Single(v => v.VendorId == id); + vendor.VendorName.ShouldBe("test vendor"); + vendor.VendorNamespacePrefixes.Select(x => x.NamespacePrefix).ShouldBe(namespacePrefixes); + vendor.Users.Single().FullName.ShouldBe("test user"); + vendor.Users.Single().Email.ShouldBe("test@test.com"); + }); + } + + [TestCase("http://www.test1.com/, http://www.test2.com/,", "http://www.test1.com/,http://www.test2.com/")] + [TestCase(", ,", "")] + [TestCase(" ", "")] + [TestCase(null, "")] + public void ShouldNotAddEmptyNamespacePrefixesWhileAddingVendor(string inputNamespacePrefixes, string expectedNamespacePrefixes) + { + var newVendor = new Mock(); + newVendor.Setup(x => x.Company).Returns("test vendor"); + newVendor.Setup(x => x.NamespacePrefixes).Returns(inputNamespacePrefixes); + newVendor.Setup(x => x.ContactName).Returns("test user"); + newVendor.Setup(x => x.ContactEmailAddress).Returns("test@test.com"); + + var id = 0; + Transaction(usersContext => + { + var command = new AddVendorCommand(usersContext); + + id = command.Execute(newVendor.Object).VendorId; + id.ShouldBeGreaterThan(0); + }); + + Transaction(usersContext => + { + var vendor = usersContext.Vendors. + Include(x => x.VendorNamespacePrefixes) + .Include(x => x.Users) + .Single(v => v.VendorId == id); + vendor.VendorName.ShouldBe("test vendor"); + vendor.VendorNamespacePrefixes.Select(x => x.NamespacePrefix).ToDelimiterSeparated().ShouldBe(expectedNamespacePrefixes); + vendor.Users.Single().FullName.ShouldBe("test user"); + vendor.Users.Single().Email.ShouldBe("test@test.com"); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/DeleteApplicationCommandTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/DeleteApplicationCommandTests.cs new file mode 100644 index 000000000..fd685fbaa --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/DeleteApplicationCommandTests.cs @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.CommandTests; + +[TestFixture] +public class DeleteApplicationCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldDeleteApplication() + { + var application = new Application { ApplicationName = "test application", OperationalContextUri = OperationalContext.DefaultOperationalContextUri }; + Save(application); + var applicationId = application.ApplicationId; + + Transaction(usersContext => + { + var deleteApplicationCommand = new DeleteApplicationCommand(usersContext); + deleteApplicationCommand.Execute(applicationId); + }); + + Transaction(usersContext => usersContext.Applications.Where(a => a.ApplicationId == applicationId).ToArray()).ShouldBeEmpty(); + } + + [Test] + public void ShouldDeleteApplicationWithClient() + { + var application = new Application { ApplicationName = "test application", OperationalContextUri = OperationalContext.DefaultOperationalContextUri }; + + var client = new ApiClient + { + Name = "test client", + Key = "n/a", + Secret = "n/a", + ActivationCode = "fake activation code" + }; + + var clientAccessToken = new ClientAccessToken + { + ApiClient = client, + Expiration = DateTime.Now.AddDays(1) + }; + + client.ClientAccessTokens.Add(clientAccessToken); + + application.ApiClients.Add(client); + Save(application); + + var applicationId = application.ApplicationId; + applicationId.ShouldBeGreaterThan(0); + + var clientId = client.ApiClientId; + clientId.ShouldBeGreaterThan(0); + + var tokenId = clientAccessToken.Id; + tokenId.ShouldNotBe(Guid.Empty); + + Transaction(usersContext => + { + var deleteApplicationCommand = new DeleteApplicationCommand(usersContext); + deleteApplicationCommand.Execute(applicationId); + }); + + Transaction(usersContext => usersContext.Applications.Where(a => a.ApplicationId == applicationId).ToArray()).ShouldBeEmpty(); + Transaction(usersContext => usersContext.Clients.Where(c => c.ApiClientId == clientId).ToArray()).ShouldBeEmpty(); + } + + [Test] + public void ShouldDeleteApplicationWithOrganization() + { + var application = new Application { ApplicationName = "test application", OperationalContextUri = OperationalContext.DefaultOperationalContextUri }; + + var client = new ApiClient + { + Name = "test client", + Key = "n/a", + Secret = "n/a", + }; + + var organization = new ApplicationEducationOrganization + { + Application = application, + Clients = [client] + }; + + application.ApiClients.Add(client); + application.ApplicationEducationOrganizations.Add(organization); + Save(application); + + var applicationId = application.ApplicationId; + applicationId.ShouldBeGreaterThan(0); + + var organizationId = organization.ApplicationEducationOrganizationId; + organizationId.ShouldBeGreaterThan(0); + + Transaction(usersContext => + { + var deleteApplicationCommand = new DeleteApplicationCommand(usersContext); + deleteApplicationCommand.Execute(applicationId); + }); + + Transaction(usersContext => usersContext.Applications.Where(a => a.ApplicationId == applicationId).ToArray()).ShouldBeEmpty(); + Transaction(usersContext => usersContext.ApplicationEducationOrganizations.Where(o => o.ApplicationEducationOrganizationId == organizationId).ToArray()).ShouldBeEmpty(); + } + + [Test] + public void ShouldDeleteApplicationWithProfile() + { + var application = new Application { ApplicationName = "test application", OperationalContextUri = OperationalContext.DefaultOperationalContextUri }; + var profile = new Profile { ProfileName = "test profile" }; + application.Profiles.Add(profile); + + Save(application); + + var applicationId = application.ApplicationId; + applicationId.ShouldBeGreaterThan(0); + + var profileId = profile.ProfileId; + profileId.ShouldBeGreaterThan(0); + + Transaction(usersContext => + { + var deleteApplicationCommand = new DeleteApplicationCommand(usersContext); + deleteApplicationCommand.Execute(applicationId); + }); + + Transaction(usersContext => usersContext.Applications.Where(a => a.ApplicationId == applicationId).ToArray()).ShouldBeEmpty(); + Transaction(usersContext => usersContext.Profiles.Where(p => p.ProfileId == profileId).ToArray()).ShouldNotBeEmpty(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/DeleteOdsInstanceCommandTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/DeleteOdsInstanceCommandTests.cs new file mode 100644 index 000000000..dffa5308c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/DeleteOdsInstanceCommandTests.cs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.CommandTests; + +[TestFixture] +public class DeleteOdsInstanceCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldDeleteOdsInstance() + { + var newOdsInstance = new OdsInstance() + { + Name = "test", + InstanceType = "type", + Status = "status", + Version = "version" + }; + Save(newOdsInstance); + var odsInstanceId = newOdsInstance.OdsInstanceId; + + Transaction(usersContext => + { + var deleteOdsInstanceCommand = new DeleteOdsInstanceCommand(usersContext); + deleteOdsInstanceCommand.Execute(odsInstanceId); + }); + + Transaction(usersContext => usersContext.OdsInstances.Where(v => v.OdsInstanceId == odsInstanceId).ToArray()).ShouldBeEmpty(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/DeleteVendorCommandTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/DeleteVendorCommandTests.cs new file mode 100644 index 000000000..8d28175c4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/DeleteVendorCommandTests.cs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using NUnit.Framework; +using Shouldly; +using VendorUser = EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models.User; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.CommandTests; + +[TestFixture] +public class DeleteVendorCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldDeleteVendor() + { + var newVendor = new Vendor(); + Save(newVendor); + var vendorId = newVendor.VendorId; + + Transaction(usersContext => + { + var deleteVendorCommand = new DeleteVendorCommand(usersContext, null); + deleteVendorCommand.Execute(vendorId); + }); + + Transaction(usersContext => usersContext.Vendors.Where(v => v.VendorId == vendorId).ToArray()).ShouldBeEmpty(); + } + + [Test] + public void ShouldDeleteVendorWithApplication() + { + var newVendor = new Vendor { VendorName = "test vendor" }; + var newApplication = new Application { ApplicationName = "test application", OperationalContextUri = OperationalContext.DefaultOperationalContextUri }; + newVendor.Applications.Add(newApplication); + Save(newVendor); + var vendorId = newVendor.VendorId; + var applicationId = newApplication.ApplicationId; + applicationId.ShouldBeGreaterThan(0); + + Transaction(usersContext => + { + var deleteApplicationCommand = new DeleteApplicationCommand(usersContext); + var deleteVendorCommand = new DeleteVendorCommand(usersContext, deleteApplicationCommand); + deleteVendorCommand.Execute(vendorId); + }); + + Transaction(usersContext => usersContext.Vendors.Where(v => v.VendorId == vendorId).ToArray()).ShouldBeEmpty(); + Transaction(usersContext => usersContext.Applications.Where(a => a.ApplicationId == applicationId).ToArray()).ShouldBeEmpty(); + } + + [Test] + public void ShouldDeleteVendorWithUser() + { + var newVendor = new Vendor { VendorName = "test vendor" }; + var newUser = new VendorUser + { + FullName = "test user", + Email = "testuser@example.com", // Required property 'Email' set + Vendor = newVendor // Required property 'Vendor' set + }; + newVendor.Users.Add(newUser); + Save(newVendor); + var vendorId = newVendor.VendorId; + var userId = newUser.UserId; + userId.ShouldBeGreaterThan(0); + + Transaction(usersContext => + { + var deleteVendorCommand = new DeleteVendorCommand(usersContext, null); + deleteVendorCommand.Execute(vendorId); + }); + + Transaction(usersContext => usersContext.Vendors.Where(v => v.VendorId == vendorId).ToArray()).ShouldBeEmpty(); + Transaction(usersContext => usersContext.Users.Where(u => u.UserId == userId).ToArray()).ShouldBeEmpty(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/EditApplicationCommandTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/EditApplicationCommandTests.cs new file mode 100644 index 000000000..ef1080cdf --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/EditApplicationCommandTests.cs @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Linq; +using System.Threading.Tasks; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using Shouldly; +using Profile = EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models.Profile; +using VendorUser = EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models.User; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.CommandTests; + +[TestFixture] +public class EditApplicationCommandTests : PlatformUsersContextTestBase +{ + private const int EdOrgId1 = 1234; + private const int EdOrgId2 = 2345; + private const int EdOrgId3 = 56666; + + private Vendor _vendor; + private Vendor _otherVendor; + private VendorUser _user; + private VendorUser _otherUser; + private Profile _profile; + private Profile _otherProfile; + private ApiClient _apiClient; + private Application _application; + + + private void SetupTestEntities() + { + _vendor = new Vendor + { + VendorNamespacePrefixes = [new VendorNamespacePrefix { NamespacePrefix = "http://tests.com", Vendor = _vendor }], + VendorName = "Integration Tests" + }; + + _otherVendor = new Vendor + { + VendorNamespacePrefixes = [new VendorNamespacePrefix { NamespacePrefix = "http://tests.com", Vendor = _otherVendor }], + VendorName = "Different Integration Tests" + }; + + _user = new VendorUser + { + Email = "nobody@nowhere.com", + FullName = "Integration Tests", + Vendor = _vendor + }; + + _otherUser = new VendorUser + { + Email = "nowhere@nobody.com", + FullName = "Different Integration Tests", + Vendor = _otherVendor + }; + + _profile = new Profile + { + ProfileName = "Test Profile" + }; + + _otherProfile = new Profile + { + ProfileName = "Other Test Profile" + }; + + _apiClient = new ApiClient(true) + { + Name = "Integration Test", + UseSandbox = false, + }; + + _application = new Application + { + ApplicationName = "Test Application", + ClaimSetName = "FakeClaimSet", + Vendor = _vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + + _application.ApiClients.Add(_apiClient); + _application.Profiles.Add(_profile); + _application.ApplicationEducationOrganizations.Add(_application.CreateApplicationEducationOrganization(EdOrgId1)); + _application.ApplicationEducationOrganizations.Add(_application.CreateApplicationEducationOrganization(EdOrgId2)); + + Save(_vendor, _otherVendor, _user, _otherUser, _profile, _otherProfile, _application); + } + + [Test] + public void ShouldRemoveProfileIfNull() + { + SetupTestEntities(); + + var editModel = new TestEditApplicationModel + { + ApplicationId = _application.ApplicationId, + ApplicationName = _application.ApplicationName, + ClaimSetName = _application.ClaimSetName, + EducationOrganizationIds = [EdOrgId1, EdOrgId2], + ProfileId = null, + VendorId = _vendor.VendorId + }; + + Transaction(usersContext => + { + var command = new EditApplicationCommand(usersContext); + command.Execute(editModel); + }); + + Transaction(usersContext => + { + var persistedApplication = usersContext.Applications + .Include(x => x.ApiClients) + .Include(x => x.ApplicationEducationOrganizations) + .Include(x => x.Profiles) + .Single(a => a.ApplicationId == _application.ApplicationId); + persistedApplication.ApplicationName.ShouldBe("Test Application"); + persistedApplication.ClaimSetName.ShouldBe("FakeClaimSet"); + persistedApplication.ApiClients.Count.ShouldBe(1); + persistedApplication.ApiClients.First().Name.ShouldBe("Test Application"); + persistedApplication.ApiClients.First().ApplicationEducationOrganizations.ShouldAllBe(aeo => persistedApplication.ApplicationEducationOrganizations.Contains(aeo)); + persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(2); + persistedApplication.ApplicationEducationOrganizations.ShouldAllBe(aeo => aeo.EducationOrganizationId == EdOrgId1 || aeo.EducationOrganizationId == EdOrgId2); + persistedApplication.Profiles.Count.ShouldBe(0); + }); + } + + [Test] + public void ShouldUpdateAllEntitiesProperly() + { + SetupTestEntities(); + + var editModel = new TestEditApplicationModel + { + ApplicationId = _application.ApplicationId, + ApplicationName = "New Application Name", + ClaimSetName = "DifferentFakeClaimSet", + EducationOrganizationIds = [EdOrgId2, EdOrgId3], + ProfileId = _otherProfile.ProfileId, + VendorId = _otherVendor.VendorId + }; + + Transaction(usersContext => + { + var command = new EditApplicationCommand(usersContext); + command.Execute(editModel); + }); + + Transaction(usersContext => + { + var persistedApplication = usersContext.Applications + .Include(x => x.ApiClients) + .Include(x => x.ApplicationEducationOrganizations) + .Include(x => x.Profiles) + .Single(a => a.ApplicationId == _application.ApplicationId); + persistedApplication.ApplicationName.ShouldBe("New Application Name"); + persistedApplication.ClaimSetName.ShouldBe("DifferentFakeClaimSet"); + persistedApplication.ApiClients.Count.ShouldBe(1); + persistedApplication.ApiClients.First().Name.ShouldBe("New Application Name"); + persistedApplication.ApiClients.First().ApplicationEducationOrganizations.ShouldAllBe(aeo => persistedApplication.ApplicationEducationOrganizations.Contains(aeo)); + persistedApplication.Profiles.Count.ShouldBe(1); + persistedApplication.Profiles.First().ProfileName.ShouldBe("Other Test Profile"); + persistedApplication.ApplicationEducationOrganizations.Count.ShouldBe(2); + persistedApplication.ApplicationEducationOrganizations.ShouldAllBe(aeo => aeo.EducationOrganizationId == EdOrgId2 || aeo.EducationOrganizationId == EdOrgId3); + }); + } + + [Test] + public void GivenAdditionalEdOrgThenItShouldBeConnectedToAllThreeEdOrgIds() + { + // Arrange + SetupTestEntities(); + + // Act + var edOrgs = _application.ApplicationEducationOrganizations.Select(x => x.EducationOrganizationId).ToList().Append(EdOrgId3); + + var editApplication = new TestEditApplicationModel + { + ApplicationId = _application.ApplicationId, + ApplicationName = _application.ApplicationName, + ClaimSetName = _application.ClaimSetName, + EducationOrganizationIds = edOrgs, + ProfileId = _application.Profiles.FirstOrDefault()?.ProfileId, + VendorId = _application.Vendor.VendorId + }; + + Transaction(usersContext => + { + var command = new EditApplicationCommand(usersContext); + command.Execute(editApplication); + }); + + // Assert + Transaction(UsersContext => + { + var aeos = UsersContext.ApplicationEducationOrganizations.ToList(); + aeos.Count.ShouldBe(3); + aeos.ShouldContain(x => x.EducationOrganizationId == EdOrgId1); + aeos.ShouldContain(x => x.EducationOrganizationId == EdOrgId2); + aeos.ShouldContain(x => x.EducationOrganizationId == EdOrgId3); + }); + } + + [Test] + public async Task GivenChangedEdOrgIdThenItShouldBeConnectedToOnlyTheOneEdOrgid() + { + // Arrange + SetupTestEntities(); + + // Act + var editApplication = new TestEditApplicationModel + { + ApplicationId = _application.ApplicationId, + ApplicationName = _application.ApplicationName, + ClaimSetName = _application.ClaimSetName, + // Now connected to just one + EducationOrganizationIds = [EdOrgId3], + ProfileId = _application.Profiles.FirstOrDefault()?.ProfileId, + VendorId = _application.Vendor.VendorId + }; + + Transaction(usersContext => + { + var command = new EditApplicationCommand(usersContext); + command.Execute(editApplication); + }); + + // Assert + Transaction(usersContext => + { + var aeos = usersContext.ApplicationEducationOrganizations.ToList(); + aeos.Count.ShouldBe(1); + var first = aeos.First(); + first.EducationOrganizationId.ShouldBe(EdOrgId3); + }); + + // Not trusting Entity Framework for the following check - directly querying the database + const string Sql = "select count(1) from dbo.ApiClientApplicationEducationOrganizations"; + using var connection = new SqlConnection(ConnectionString); + await connection.OpenAsync(); + using var command = new SqlCommand(Sql, connection); + var count = (int)await command.ExecuteScalarAsync(); + count.ShouldBe(1); + } + + private class TestEditApplicationModel : IEditApplicationModel + { + public int ApplicationId { get; set; } + public string ApplicationName { get; set; } + public int VendorId { get; set; } + public string ClaimSetName { get; set; } + public int? ProfileId { get; set; } + public int? OdsInstanceId { get; set; } + public IEnumerable EducationOrganizationIds { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/EditOdsInstanceCommandTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/EditOdsInstanceCommandTests.cs new file mode 100644 index 000000000..903283924 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/EditOdsInstanceCommandTests.cs @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Moq; +using NUnit.Framework; +using Shouldly; +using System.Linq; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.CommandTests; + +[TestFixture] +internal class EditOdsInstanceCommandTests : PlatformUsersContextTestBase +{ + private int _odsInstanceId; + + [SetUp] + public void Init() + { + var originalOdsInstance = new OdsInstance + { + Name = "old ods instance name", + InstanceType = "old type", + Status = "old status", + IsExtended = false, + Version = "old version", + }; + + Save(originalOdsInstance); + _odsInstanceId = originalOdsInstance.OdsInstanceId; + } + + [Test] + public void ShouldEditVendor() + { + var newOdsInstanceData = new Mock(); + newOdsInstanceData.Setup(v => v.OdsInstanceId).Returns(_odsInstanceId); + newOdsInstanceData.Setup(v => v.Name).Returns("new ods instance name"); + newOdsInstanceData.Setup(v => v.InstanceType).Returns("new type"); + newOdsInstanceData.Setup(v => v.Status).Returns("new status"); + newOdsInstanceData.Setup(v => v.IsExtended).Returns(true); + newOdsInstanceData.Setup(v => v.Version).Returns("new version"); + + Transaction(usersContext => + { + var editOdsInstanceCommand = new EditOdsInstanceCommand(usersContext); + editOdsInstanceCommand.Execute(newOdsInstanceData.Object); + }); + + Transaction(usersContext => + { + var changedOdsInstance = usersContext.OdsInstances + .Single(v => v.OdsInstanceId == _odsInstanceId); + changedOdsInstance.Name.ShouldBe("new ods instance name"); + changedOdsInstance.InstanceType.ShouldBe("new type"); + changedOdsInstance.Status.ShouldBe("new status"); + changedOdsInstance.IsExtended.ShouldBe(true); + changedOdsInstance.Version.ShouldBe("new version"); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/EditVendorCommandTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/EditVendorCommandTests.cs new file mode 100644 index 000000000..8ac6ae02c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/EditVendorCommandTests.cs @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.V1.Infrastructure.Helpers; +using Microsoft.EntityFrameworkCore; +using Moq; +using NUnit.Framework; +using Shouldly; +using VendorUser = EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models.User; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.CommandTests; + +[TestFixture] +internal class EditVendorCommandTests : PlatformUsersContextTestBase +{ + private int _vendorId; + private int _vendorWithNoNameSpaceId; + private const string OriginalVendorNamespacePrefix = "old namespace prefix"; + + [SetUp] + public void Init() + { + var originalVendor = new Vendor + { + VendorName = "old vendor name", + VendorNamespacePrefixes = [new VendorNamespacePrefix { NamespacePrefix = OriginalVendorNamespacePrefix, Vendor = new Vendor { VendorName = "Integration Tests" } }], + }; + var originalVendorWithNoNameSpace = new Vendor + { + VendorName = "old vendor name", + VendorNamespacePrefixes = [] + }; + var originalVendorContact = new VendorUser + { + FullName = "old contact name", + Email = "old contact email", + Vendor = originalVendor + }; + originalVendor.Users.Add(originalVendorContact); + originalVendorWithNoNameSpace.Users.Add(originalVendorContact); + + Save(originalVendor, originalVendorWithNoNameSpace); + _vendorId = originalVendor.VendorId; + _vendorWithNoNameSpaceId = originalVendorWithNoNameSpace.VendorId; + } + + [Test] + public void ShouldEditVendorWithContact() + { + var newVendorData = new Mock(); + newVendorData.Setup(v => v.VendorId).Returns(_vendorId); + newVendorData.Setup(v => v.Company).Returns("new vendor name"); + newVendorData.Setup(v => v.NamespacePrefixes).Returns("new namespace prefix"); + newVendorData.Setup(v => v.ContactName).Returns("new contact name"); + newVendorData.Setup(v => v.ContactEmailAddress).Returns("new contact email"); + + Transaction(usersContext => + { + var editVendorCommand = new EditVendorCommand(usersContext); + editVendorCommand.Execute(newVendorData.Object); + }); + + Transaction(usersContext => + { + var changedVendor = usersContext.Vendors + .Include(x => x.VendorNamespacePrefixes) + .Include(x => x.Users) + .Single(v => v.VendorId == _vendorId); + changedVendor.VendorName.ShouldBe("new vendor name"); + changedVendor.VendorNamespacePrefixes.First().NamespacePrefix.ShouldBe("new namespace prefix"); + changedVendor.Users.First().FullName.ShouldBe("new contact name"); + changedVendor.Users.First().Email.ShouldBe("new contact email"); + }); + } + + [Test] + public void ShouldEditVendorWithNoNameSpacePrefix() + { + var newVendorData = new Mock(); + newVendorData.Setup(v => v.VendorId).Returns(_vendorId); + newVendorData.Setup(v => v.Company).Returns("new vendor name"); + newVendorData.Setup(v => v.NamespacePrefixes).Returns(string.Empty); + newVendorData.Setup(v => v.ContactName).Returns("new contact name"); + newVendorData.Setup(v => v.ContactEmailAddress).Returns("new contact email"); + + Transaction(usersContext => + { + var editVendorCommand = new EditVendorCommand(usersContext); + editVendorCommand.Execute(newVendorData.Object); + }); + + Transaction(usersContext => + { + var changedVendor = usersContext.Vendors + .Include(x => x.VendorNamespacePrefixes) + .Include(x => x.Users).Single(v => v.VendorId == _vendorId); + changedVendor.VendorName.ShouldBe("new vendor name"); + changedVendor.VendorNamespacePrefixes.ShouldBeEmpty(); + changedVendor.Users.First().FullName.ShouldBe("new contact name"); + changedVendor.Users.First().Email.ShouldBe("new contact email"); + }); + } + + [Test] + public void ShouldEditVendorByAddingNewNameSpacePrefix() + { + var newVendorData = new Mock(); + newVendorData.Setup(v => v.VendorId).Returns(_vendorWithNoNameSpaceId); + newVendorData.Setup(v => v.Company).Returns("new vendor name"); + newVendorData.Setup(v => v.NamespacePrefixes).Returns("new namespace prefix"); + newVendorData.Setup(v => v.ContactName).Returns("new contact name"); + newVendorData.Setup(v => v.ContactEmailAddress).Returns("new contact email"); + + Transaction(usersContext => + { + var editVendorCommand = new EditVendorCommand(usersContext); + editVendorCommand.Execute(newVendorData.Object); + }); + + Transaction(usersContext => + { + var changedVendor = usersContext.Vendors + .Include(x => x.VendorNamespacePrefixes) + .Include(x => x.Users). + Single(v => v.VendorId == _vendorWithNoNameSpaceId); + changedVendor.VendorName.ShouldBe("new vendor name"); + changedVendor.VendorNamespacePrefixes.First().NamespacePrefix.ShouldBe("new namespace prefix"); + changedVendor.Users.First().FullName.ShouldBe("new contact name"); + changedVendor.Users.First().Email.ShouldBe("new contact email"); + }); + } + + [Test] + public void ShouldEditVendorByAddingMultipleNameSpacePrefixes() + { + var newVendorData = new Mock(); + Transaction(usersContext => + { + var originalVendor = usersContext.Vendors + .Include(x => x.VendorNamespacePrefixes).Single(v => v.VendorId == _vendorId); + originalVendor.VendorNamespacePrefixes.Single().NamespacePrefix.ShouldBe(OriginalVendorNamespacePrefix); + }); + var newNamespacePrefixes = new List + { + + "http://www.test1.com/", + "http://www.test2.com/", + "http://www.test3.com/" + }; + newVendorData.Setup(v => v.VendorId).Returns(_vendorId); + newVendorData.Setup(v => v.Company).Returns("new vendor name"); + newVendorData.Setup(v => v.NamespacePrefixes).Returns(newNamespacePrefixes.ToDelimiterSeparated()); + newVendorData.Setup(v => v.ContactName).Returns("new contact name"); + newVendorData.Setup(v => v.ContactEmailAddress).Returns("new contact email"); + + Transaction(usersContext => + { + var editVendorCommand = new EditVendorCommand(usersContext); + editVendorCommand.Execute(newVendorData.Object); + }); + + Transaction(usersContext => + { + var changedVendor = usersContext.Vendors + .Include(x => x.VendorNamespacePrefixes) + .Include(x => x.Users) + .Single(v => v.VendorId == _vendorId); + changedVendor.VendorName.ShouldBe("new vendor name"); + changedVendor.VendorNamespacePrefixes.Select(x => x.NamespacePrefix).ShouldBe(newNamespacePrefixes); + changedVendor.Users.First().FullName.ShouldBe("new contact name"); + changedVendor.Users.First().Email.ShouldBe("new contact email"); + }); + } + + [TestCase("http://www.test1.com/, http://www.test2.com/,", "http://www.test1.com/,http://www.test2.com/")] + [TestCase(", ,", "")] + [TestCase(" ", "")] + [TestCase(null, "")] + public void ShouldNotAddEmptyNameSpacePrefixesWhileEditingVendor(string inputNamespacePrefixes, string expectedNamespacePrefixes) + { + var newVendorData = new Mock(); + Transaction(usersContext => + { + var originalVendor = usersContext.Vendors + .Include(x => x.VendorNamespacePrefixes) + .Single(v => v.VendorId == _vendorId); + originalVendor.VendorNamespacePrefixes.Single().NamespacePrefix.ShouldBe(OriginalVendorNamespacePrefix); + }); + + newVendorData.Setup(v => v.VendorId).Returns(_vendorId); + newVendorData.Setup(v => v.Company).Returns("new vendor name"); + newVendorData.Setup(v => v.NamespacePrefixes).Returns(inputNamespacePrefixes); + newVendorData.Setup(v => v.ContactName).Returns("new contact name"); + newVendorData.Setup(v => v.ContactEmailAddress).Returns("new contact email"); + + Transaction(usersContext => + { + var editVendorCommand = new EditVendorCommand(usersContext); + editVendorCommand.Execute(newVendorData.Object); + }); + + Transaction(usersContext => + { + var changedVendor = usersContext.Vendors + .Include(x => x.Users) + .Include(x => x.VendorNamespacePrefixes) + .Single(v => v.VendorId == _vendorId); + changedVendor.VendorName.ShouldBe("new vendor name"); + changedVendor.VendorNamespacePrefixes.Select(x => x.NamespacePrefix).ToDelimiterSeparated().ShouldBe(expectedNamespacePrefixes); + changedVendor.Users.First().FullName.ShouldBe("new contact name"); + changedVendor.Users.First().Email.ShouldBe("new contact email"); + }); + } + + [Test] + public void ShouldEditVendorByRemovingNameSpacePrefix() + { + var newVendorData = new Mock(); + + Transaction(usersContext => + { + var originalVendor = usersContext.Vendors + .Include(x => x.VendorNamespacePrefixes) + .Single(v => v.VendorId == _vendorId); + originalVendor.VendorNamespacePrefixes.Single().NamespacePrefix.ShouldBe(OriginalVendorNamespacePrefix); + }); + + newVendorData.Setup(v => v.VendorId).Returns(_vendorId); + newVendorData.Setup(v => v.Company).Returns("new vendor name"); + newVendorData.Setup(v => v.NamespacePrefixes).Returns(""); + newVendorData.Setup(v => v.ContactName).Returns("new contact name"); + newVendorData.Setup(v => v.ContactEmailAddress).Returns("new contact email"); + + Transaction(usersContext => + { + var editVendorCommand = new EditVendorCommand(usersContext); + editVendorCommand.Execute(newVendorData.Object); + }); + + Transaction(usersContext => + { + var changedVendor = usersContext.Vendors + .Include(x => x.VendorNamespacePrefixes) + .Include(x => x.Users) + .Single(v => v.VendorId == _vendorId); + changedVendor.VendorName.ShouldBe("new vendor name"); + changedVendor.VendorNamespacePrefixes.ShouldBeEmpty(); + changedVendor.Users.First().FullName.ShouldBe("new contact name"); + changedVendor.Users.First().Email.ShouldBe("new contact email"); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/RegenerateApiClientSecretCommandTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/RegenerateApiClientSecretCommandTests.cs new file mode 100644 index 000000000..04e40a8d4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/CommandTests/RegenerateApiClientSecretCommandTests.cs @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using NUnit.Framework; +using Shouldly; +using VendorUser = EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models.User; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.CommandTests; + +[TestFixture] +public class RegenerateApiClientSecretCommandTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldFailIfApplicationDoesNotExist() + { + Transaction(usersContext => + { + var command = new RegenerateApiClientSecretCommand(usersContext); + Assert.Throws>(() => command.Execute(0)); + }); + } + + [Test] + public void ShouldReportFailureIfApiClientDoesNotExist() + { + var application = new Application + { + ApplicationName = "Api Client Secret Test App", + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + + Save(application); + + Transaction(usersContext => + { + var command = new RegenerateApiClientSecretCommand(usersContext); + Assert.Throws(() => command.Execute(application.ApplicationId)); + }); + } + + [Test] + public void ShouldUpdateApiClientSecret() + { + var vendor = new Vendor + { + VendorNamespacePrefixes = [new VendorNamespacePrefix { NamespacePrefix = "http://tests.com", Vendor = new Vendor { VendorName = "Integration Tests" } }], + VendorName = "Integration Tests" + }; + + var user = new VendorUser + { + Email = "nobody@nowhere.com", + FullName = "Integration Tests", + Vendor = vendor + }; + + var profile = new Profile + { + ProfileName = "Test Profile" + }; + + var apiClient = new ApiClient(true) + { + Name = "Integration Test" + }; + + var application = new Application + { + ApplicationName = "Test Application", + ClaimSetName = "FakeClaimSet", + ApiClients = [], + Vendor = vendor, + Profiles = [], + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + + application.ApiClients.Add(apiClient); + application.Profiles.Add(profile); + + Save(vendor, user, profile, application); + + var orignalKey = apiClient.Key; + var originalSecret = apiClient.Secret; + + //Simulate the automatic hashing performed by using the key/secret on the API. + Transaction(usersContext => + { + var odsSideApiClient = usersContext.Clients.Single(c => c.ApiClientId == apiClient.ApiClientId); + odsSideApiClient.Secret = "SIMULATED HASH OF " + originalSecret; + odsSideApiClient.SecretIsHashed = true; + }); + + RegenerateApiClientSecretResult result = null; + Transaction(usersContext => + { + var command = new RegenerateApiClientSecretCommand(usersContext); + result = command.Execute(application.ApplicationId); + }); + + var updatedApiClient = Transaction(usersContext => usersContext.Clients.Single(c => c.ApiClientId == apiClient.ApiClientId)); + + result.Key.ShouldBe(orignalKey); + result.Secret.ShouldNotBe(originalSecret); + result.Secret.ShouldNotBe("SIMULATED HASH OF " + originalSecret); + result.Secret.ShouldNotBeEmpty(); + + updatedApiClient.Key.ShouldBe(result.Key); + updatedApiClient.Secret.ShouldBe(result.Secret); + updatedApiClient.SecretIsHashed.ShouldBe(false); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetApplicationsByVendorIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetApplicationsByVendorIdQueryTests.cs new file mode 100644 index 000000000..18628d563 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetApplicationsByVendorIdQueryTests.cs @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.QueryTests; + +[TestFixture] +public class GetApplicationsByVendorIdQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldGetBasicApplicationData() + { + var vendor = new Vendor { VendorName = "test vendor" }; + var application = new Application + { + ApplicationName = "test application", + ClaimSetName = "test claim set", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + vendor.Applications.Add(application); + Save(vendor); + + Transaction(usersContext => + { + var getApplicationsByVendorIdQuery = new GetApplicationsByVendorIdQuery(usersContext); + var results = getApplicationsByVendorIdQuery.Execute(vendor.VendorId); + results.Single().ApplicationName.ShouldBe("test application"); + results.Single().ClaimSetName.ShouldBe("test claim set"); + }); + } + + [Test] + public void ShouldGetApplicationEducationOrganization() + { + var vendor = new Vendor { VendorName = "test vendor" }; + var application = new Application + { + ApplicationName = "test application", + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + + var applicationOrganization = new ApplicationEducationOrganization { Application = application }; + + application.ApplicationEducationOrganizations.Add(applicationOrganization); + vendor.Applications.Add(application); + Save(vendor); + var organizationId = applicationOrganization.ApplicationEducationOrganizationId; + organizationId.ShouldBeGreaterThan(0); + + Transaction(usersContext => + { + var getApplicationsByVendorIdQuery = new GetApplicationsByVendorIdQuery(usersContext); + var results = getApplicationsByVendorIdQuery.Execute(vendor.VendorId); + results.Single().ApplicationEducationOrganizations.Single().ApplicationEducationOrganizationId.ShouldBe(organizationId); + }); + } + + [Test] + public void ShouldGetApplicationProfile() + { + var vendor = new Vendor { VendorName = "test vendor" }; + var application = new Application { ApplicationName = "test application", OperationalContextUri = OperationalContext.DefaultOperationalContextUri }; + var profile = new Profile + { + Applications = [application], + ProfileName = "test profile" + }; + application.Profiles.Add(profile); + vendor.Applications.Add(application); + Save(vendor); + var profileId = profile.ProfileId; + profileId.ShouldBeGreaterThan(0); + + Transaction(usersContext => + { + var getApplicationsByVendorIdQuery = new GetApplicationsByVendorIdQuery(usersContext); + var results = getApplicationsByVendorIdQuery.Execute(vendor.VendorId); + results.Single().Profiles.Single().ProfileId.ShouldBe(profileId); + }); + } + + [Test] + public void ShouldThrowWhenVendorIdIsInvalid() + { + Transaction(usersContext => + { + var getApplicationsByVendorIdQuery = new GetApplicationsByVendorIdQuery(usersContext); + Should.Throw>(() => + { + getApplicationsByVendorIdQuery.Execute(int.MaxValue); + }); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationsQueryTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetApplicationsQueryTests.cs similarity index 80% rename from Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationsQueryTests.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetApplicationsQueryTests.cs index 16c251458..3f1af38fd 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetApplicationsQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetApplicationsQueryTests.cs @@ -3,16 +3,14 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System.Collections.Generic; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; using NUnit.Framework; using Shouldly; -using System.Linq; -using EdFi.Admin.DataAccess.Models; -using EdFi.Ods.AdminApi.Infrastructure; -using AutoMapper; -namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.QueryTests; [TestFixture] public class GetApplicationsQueryTests : PlatformUsersContextTestBase @@ -23,7 +21,7 @@ public void Should_retrieve_vendors() var newVendor = new Vendor { VendorName = "test vendor", - VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://testvendor.net" } }, + VendorNamespacePrefixes = [new VendorNamespacePrefix { NamespacePrefix = "http://testvendor.net", Vendor = new Vendor { VendorName = "Integration Tests" } }], }; Save(newVendor); @@ -51,23 +49,23 @@ public void Should_retrieve_applications_with_offset_and_limit() vendors[vendorIndex] = new Vendor { VendorName = $"test vendor {vendorIndex + 1}", - VendorNamespacePrefixes = new List { new VendorNamespacePrefix { NamespacePrefix = "http://testvendor.net" } }, - Applications = new List - { + VendorNamespacePrefixes = [new VendorNamespacePrefix { NamespacePrefix = "http://testvendor.net", Vendor = new Vendor { VendorName = "Integration Tests" } }], + Applications = + [ new Application { ApplicationName = $"test app {vendorIndex + 1}", ClaimSetName = "Ed-Fi API Publisher - Reader", OperationalContextUri = $"test app {vendorIndex + 1}", - ApplicationEducationOrganizations = new List - { + ApplicationEducationOrganizations = + [ new ApplicationEducationOrganization { EducationOrganizationId = 0, } - } + ] } - } + ] }; } diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetChildResourceClaimsForParentQueryTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetChildResourceClaimsForParentQueryTests.cs new file mode 100644 index 000000000..2f5f4ca9d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetChildResourceClaimsForParentQueryTests.cs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using NUnit.Framework; +using Shouldly; +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.QueryTests; + +[TestFixture] +public class GetChildResourceClaimsForParentQueryTests : SecurityDataTestBase +{ + [Test] + public void ShouldGetResourceClaims() + { + var testApplication = new Application + { + ApplicationName = "TestApplicationName" + }; + + Save(testApplication); + + var parentRcs = UniqueNameList("Parent", 2); + + var childRcs = UniqueNameList("Child", 1); + + var testResourceClaims = SetupResourceClaims(testApplication, parentRcs, childRcs); + + var testParentResource = testResourceClaims.Single(x => x.ResourceName == parentRcs.First()); + + ResourceClaim[] results = null; + Transaction(securityContext => + { + var query = new GetChildResourceClaimsForParentQuery(securityContext); + + results = query.Execute(testParentResource.ResourceClaimId).ToArray(); + }); + + Transaction(securityContext => + { + var testChildResourceClaims = securityContext.ResourceClaims.Where(x => + x.ParentResourceClaimId == testParentResource.ResourceClaimId); + + results.Length.ShouldBe(testChildResourceClaims.Count()); + results.Select(x => x.Name).ShouldBe(testChildResourceClaims.Select(x => x.ResourceName), true); + results.Select(x => x.Id).ShouldBe(testChildResourceClaims.Select(x => x.ResourceClaimId), true); + results.All(x => x.Create == false).ShouldBe(true); + results.All(x => x.Delete == false).ShouldBe(true); + results.All(x => x.Update == false).ShouldBe(true); + results.All(x => x.Read == false).ShouldBe(true); + results.All(x => x.ParentId.Equals(testParentResource.ResourceClaimId)).ShouldBe(true); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetOdsInstancesQueryTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetOdsInstancesQueryTests.cs new file mode 100644 index 000000000..15529851b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetOdsInstancesQueryTests.cs @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.QueryTests; + +[TestFixture] +public class GetOdsInstancesQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void Should_retrieve_ods_instances() + { + var newOdsInstance = new OdsInstance + { + Name = "test ods instance", + InstanceType = "test ods instance type", + Status = "OK", + Version = "1.0.0", + }; + + Save(newOdsInstance); + + Transaction(usersContext => + { + var command = new GetOdsInstancesQuery(usersContext, Testing.GetAppSettings()); + var allOdsInstances = command.Execute(); + + allOdsInstances.ShouldNotBeEmpty(); + + var odsInstance = allOdsInstances.Single(v => v.OdsInstanceId == newOdsInstance.OdsInstanceId); + odsInstance.Name.ShouldBe("test ods instance"); + odsInstance.InstanceType.ShouldBe("test ods instance type"); + }); + } + + [Test] + public void Should_retrieve_ods_instances_with_offset_and_limit() + { + var odsInstances = new OdsInstance[5]; + + for (var odsInstanceIndex = 0; odsInstanceIndex < 5; odsInstanceIndex++) + { + odsInstances[odsInstanceIndex] = new OdsInstance + { + Name = $"test ods instance {odsInstanceIndex + 1}", + InstanceType = "test ods instance type", + Status = "OK", + Version = "1.0.0", + }; + } + + Save(odsInstances); + + Transaction(usersContext => + { + var command = new GetOdsInstancesQuery(usersContext, Testing.GetAppSettings()); + var commonQueryParams = new CommonQueryParams(0, 2); + + var odsInstancesAfterOffset = command.Execute(commonQueryParams); + + odsInstancesAfterOffset.ShouldNotBeEmpty(); + odsInstancesAfterOffset.Count.ShouldBe(2); + + odsInstancesAfterOffset.ShouldContain(v => v.Name == "test ods instance 1"); + odsInstancesAfterOffset.ShouldContain(v => v.Name == "test ods instance 2"); + + commonQueryParams.Offset = 2; + + odsInstancesAfterOffset = command.Execute(commonQueryParams); + + odsInstancesAfterOffset.ShouldNotBeEmpty(); + odsInstancesAfterOffset.Count.ShouldBe(2); + + odsInstancesAfterOffset.ShouldContain(v => v.Name == "test ods instance 3"); + odsInstancesAfterOffset.ShouldContain(v => v.Name == "test ods instance 4"); + commonQueryParams.Offset = 4; + + odsInstancesAfterOffset = command.Execute(commonQueryParams); + + odsInstancesAfterOffset.ShouldNotBeEmpty(); + odsInstancesAfterOffset.Count.ShouldBe(1); + + odsInstancesAfterOffset.ShouldContain(v => v.Name == "test ods instance 5"); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetProfilesQueryTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetProfilesQueryTests.cs new file mode 100644 index 000000000..d55251fd1 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetProfilesQueryTests.cs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.QueryTests; + +[TestFixture] +public class GetProfilesQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void Should_retreive_profiles() + { + var profile1 = CreateProfile(); + var profile2 = CreateProfile(); + + Save(profile1, profile2); + + List results = null; + Transaction(usersContext => + { + var query = new GetProfilesQuery(usersContext); + results = query.Execute(); + }); + + results.Any(p => p.ProfileName == profile1.ProfileName).ShouldBeTrue(); + results.Any(p => p.ProfileName == profile2.ProfileName).ShouldBeTrue(); + } + + private static int _profileId = 0; + private static Profile CreateProfile() + { + return new Profile + { + ProfileName = $"Test Profile {_profileId++}-{DateTime.Now:O}" + }; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetResourceClaimsAsFlatListQueryTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetResourceClaimsAsFlatListQueryTests.cs new file mode 100644 index 000000000..951adbf97 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetResourceClaimsAsFlatListQueryTests.cs @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; +using NUnit.Framework; +using Shouldly; + +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; +using ResourceClaim = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ResourceClaim; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.QueryTests; + +[TestFixture] +public class GetResourceClaimsAsFlatListQueryTests : SecurityDataTestBase +{ + [Test] + public void ShouldGetResourceClaimsAsFlatList() + { + var testApplication = new Application + { + ApplicationName = "TestApplicationName" + }; + + Save(testApplication); + + var testResourceClaims = SetupResourceClaims(testApplication); + + V1.Infrastructure.Services.ClaimSetEditor.ResourceClaim[] results = null; + using var securityContext = TestContext; + var query = new GetResourceClaimsAsFlatListQuery(securityContext); + results = [.. query.Execute()]; + results.Length.ShouldBe(testResourceClaims.Count); + results.Select(x => x.Name).ShouldBe(testResourceClaims.Select(x => x.ResourceName), true); + results.Select(x => x.Id).ShouldBe(testResourceClaims.Select(x => x.ResourceClaimId), true); + results.All(x => x.Create == false).ShouldBe(true); + results.All(x => x.Delete == false).ShouldBe(true); + results.All(x => x.Update == false).ShouldBe(true); + results.All(x => x.Read == false).ShouldBe(true); + results.All(x => x.ParentId.Equals(0)).ShouldBe(true); + results.All(x => x.ParentName == null).ShouldBe(true); + results.All(x => x.Children.Count == 0).ShouldBe(true); + } + + [Test] + public void ShouldGetAlphabeticallySortedFlatListForResourceClaims() + { + var testApplication = new Application + { + ApplicationName = "TestApplicationName" + }; + + Save(testApplication); + + var testClaimSet = new ClaimSet + { ClaimSetName = "TestClaimSet_test", Application = testApplication }; + Save(testClaimSet); + var testResourceClaims = SetupParentResourceClaimsWithChildren(testClaimSet, testApplication, UniqueNameList("ParentRc", 3), UniqueNameList("ChildRc", 1)).ToList(); + var parentResourceNames = testResourceClaims.Where(x => x.ResourceClaim?.ParentResourceClaim == null) + .OrderBy(x => x.ResourceClaim.ResourceName).Select(x => x.ResourceClaim?.ResourceName).ToList(); + var childResourceNames = testResourceClaims.Where(x => x.ResourceClaim?.ParentResourceClaim != null) + .OrderBy(x => x.ResourceClaim?.ResourceName).Select(x => x.ResourceClaim?.ResourceName).ToList(); + + List results = null; + using var securityContext = TestContext; + var query = new GetResourceClaimsAsFlatListQuery(securityContext); + results = [.. query.Execute()]; + results.Count.ShouldBe(testResourceClaims.Count); + results.Where(x => x.ParentId == 0).Select(x => x.Name).ToList().ShouldBe(parentResourceNames); + results.Where(x => x.ParentId != 0).Select(x => x.Name).ToList().ShouldBe(childResourceNames); + } + + private IReadOnlyCollection SetupResourceClaims(Application testApplication, int resourceClaimCount = 5) + { + var resourceClaims = new List(); + foreach (var index in Enumerable.Range(1, resourceClaimCount)) + { + var resourceClaim = new ResourceClaim + { + ClaimName = $"TestResourceClaim{index:N}", + DisplayName = $"TestResourceClaim{index:N}", + ResourceName = $"TestResourceClaim{index:N}", + Application = testApplication + }; + resourceClaims.Add(resourceClaim); + } + + Save(resourceClaims.Cast().ToArray()); + + return resourceClaims; + } + + //private IReadOnlyCollection SetupParentResourceClaimsWithChildren(Application testApplication, int resourceClaimCount = 5, int childResourceClaimCount = 3) + //{ + // var parentResourceClaims = Enumerable.Range(1, resourceClaimCount).Select(parentIndex => new ResourceClaim + // { + // ClaimName = $"TestParentResourceClaim{parentIndex}", + // DisplayName = $"TestParentResourceClaim{parentIndex}", + // ResourceName = $"TestParentResourceClaim{parentIndex}", + // Application = testApplication + // }).ToList(); + + // var childResourceClaims = parentResourceClaims.SelectMany(x => Enumerable.Range(1, childResourceClaimCount) + // .Select(childIndex => new ResourceClaim + // { + // ClaimName = $"TestChildResourceClaim{childIndex}", + // DisplayName = $"TestChildResourceClaim{childIndex}", + // ResourceName = $"TestChildResourceClaim{childIndex}", + // Application = testApplication, + // ParentResourceClaim = x + // })).ToList(); + + // Save(childResourceClaims.Cast().ToArray()); + // parentResourceClaims.AddRange(childResourceClaims); + // return parentResourceClaims; + //} +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetResourceClaimsQueryTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetResourceClaimsQueryTests.cs new file mode 100644 index 000000000..1129b81d9 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetResourceClaimsQueryTests.cs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; +using Application = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Application; +using ResourceClaim = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ResourceClaim; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.QueryTests; + +[TestFixture] +public class GetResourceClaimsQueryTests : SecurityDataTestBase +{ + [Test] + public void ShouldGetResourceClaims() + { + var testApplication = new Application + { + ApplicationName = "TestApplicationName" + }; + + Save(testApplication); + + var testResourceClaims = SetupResourceClaims(testApplication); + + V1.Infrastructure.Services.ClaimSetEditor.ResourceClaim[] results = null; + using var securityContext = TestContext; + var query = new GetResourceClaimsQuery(securityContext); + results = query.Execute().ToArray(); + results.Length.ShouldBe(testResourceClaims.Count); + results.Select(x => x.Name).ShouldBe(testResourceClaims.Select(x => x.ResourceName), true); + results.Select(x => x.Id).ShouldBe(testResourceClaims.Select(x => x.ResourceClaimId), true); + results.All(x => x.Create == false).ShouldBe(true); + results.All(x => x.Delete == false).ShouldBe(true); + results.All(x => x.Update == false).ShouldBe(true); + results.All(x => x.Read == false).ShouldBe(true); + results.All(x => x.ParentId.Equals(0)).ShouldBe(true); + results.All(x => x.ParentName == null).ShouldBe(true); + results.All(x => x.Children.Count == 0).ShouldBe(true); + } + + private IReadOnlyCollection SetupResourceClaims(Application testApplication, int resourceClaimCount = 5) + { + var resourceClaims = new List(); + foreach (var index in Enumerable.Range(1, resourceClaimCount)) + { + var resourceClaim = new ResourceClaim + { + ClaimName = $"TestResourceClaim{index:N}", + DisplayName = $"TestResourceClaim{index:N}", + ResourceName = $"TestResourceClaim{index:N}", + Application = testApplication + }; + resourceClaims.Add(resourceClaim); + } + + Save(resourceClaims.Cast().ToArray()); + + return resourceClaims; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetVendorByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetVendorByIdQueryTests.cs new file mode 100644 index 000000000..822106556 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetVendorByIdQueryTests.cs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.QueryTests; + +[TestFixture] +public class GetVendorByIdQueryTests : PlatformUsersContextTestBase +{ + [Test] + public void ShouldGetVendorById() + { + Vendor nullResult = null; + Transaction(usersContext => + { + var query = new GetVendorByIdQuery(usersContext); + nullResult = query.Execute(0); + }); + nullResult.ShouldBeNull(); + + var vendor = new Vendor { VendorName = "test vendor" }; + Save(vendor); + + Vendor results = null; + Transaction(usersContext => + { + var query = new GetVendorByIdQuery(usersContext); + results = query.Execute(vendor.VendorId); + }); + results.VendorId.ShouldBe(vendor.VendorId); + results.VendorName.ShouldBe("test vendor"); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetVendorsQueryTests.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetVendorsQueryTests.cs new file mode 100644 index 000000000..686fe7658 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Database/QueryTests/GetVendorsQueryTests.cs @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V1.DBTests.Database.QueryTests; + +[TestFixture] +public class GetVendorsQueryTests : PlatformUsersContextTestBase +{ + // Fix for CS9035: Set the required 'Vendor' property in the object initializer. + + [Test] + public void Should_retrieve_vendors() + { + var newVendor = new Vendor + { + VendorName = "test vendor", + VendorNamespacePrefixes = + [ + new() { + NamespacePrefix = "http://testvendor.net", + Vendor = new Vendor() + } + ], + }; + + Save(newVendor); + + Transaction(usersContext => + { + var command = new GetVendorsQuery(usersContext, Testing.GetAppSettings()); + var allVendors = command.Execute(); + + allVendors.ShouldNotBeEmpty(); + + var vendor = allVendors.Single(v => v.VendorId == newVendor.VendorId); + vendor.VendorName.ShouldBe("test vendor"); + vendor.VendorNamespacePrefixes.First().NamespacePrefix.ShouldBe("http://testvendor.net"); + }); + } + + [Test] + public void Should_retrieve_vendors_with_offset_and_limit() + { + var vendors = new Vendor[5]; + + for (var vendorIndex = 0; vendorIndex < 5; vendorIndex++) + { + vendors[vendorIndex] = new Vendor + { + VendorName = $"test vendor {vendorIndex + 1}", + VendorNamespacePrefixes = + [ + new() { + NamespacePrefix = "http://testvendor.net", + Vendor = new Vendor() + } + ] + }; + } + + Save(vendors); + + Transaction(usersContext => + { + var command = new GetVendorsQuery(usersContext, Testing.GetAppSettings()); + var commonQueryParams = new CommonQueryParams(0, 2); + + var vendorsAfterOffset = command.Execute(commonQueryParams); + + vendorsAfterOffset.ShouldNotBeEmpty(); + vendorsAfterOffset.Count.ShouldBe(2); + + vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 1"); + vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 2"); + + commonQueryParams.Offset = 2; + + vendorsAfterOffset = command.Execute(commonQueryParams); + + vendorsAfterOffset.ShouldNotBeEmpty(); + vendorsAfterOffset.Count.ShouldBe(2); + + vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 3"); + vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 4"); + commonQueryParams.Offset = 4; + + vendorsAfterOffset = command.Execute(commonQueryParams); + + vendorsAfterOffset.ShouldNotBeEmpty(); + vendorsAfterOffset.Count.ShouldBe(1); + + vendorsAfterOffset.ShouldContain(v => v.VendorName == "test vendor 5"); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/EdFi.Ods.AdminApi.DBTests.csproj b/Application/EdFi.Ods.AdminApi.V1.DBTests/EdFi.Ods.AdminApi.DBTests.csproj new file mode 100644 index 000000000..2c78d7f46 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/EdFi.Ods.AdminApi.DBTests.csproj @@ -0,0 +1,33 @@ + + + net8.0 + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + Always + + + diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/EdFi.Ods.AdminApi.V1.DBTests.csproj b/Application/EdFi.Ods.AdminApi.V1.DBTests/EdFi.Ods.AdminApi.V1.DBTests.csproj new file mode 100644 index 000000000..2c78d7f46 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/EdFi.Ods.AdminApi.V1.DBTests.csproj @@ -0,0 +1,33 @@ + + + net8.0 + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + Always + + + diff --git a/Application/EdFi.Ods.AdminApi.DBTests/PlatformSecurityContext53TestBase.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/PlatformSecurityContextTestBase.cs similarity index 64% rename from Application/EdFi.Ods.AdminApi.DBTests/PlatformSecurityContext53TestBase.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/PlatformSecurityContextTestBase.cs index ad0812139..643078fdc 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/PlatformSecurityContext53TestBase.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/PlatformSecurityContextTestBase.cs @@ -2,24 +2,21 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; using System; using System.Threading.Tasks; -using EdFi.Admin.DataAccess.Contexts; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; +using Microsoft.EntityFrameworkCore; using NUnit.Framework; -using Respawn; -using Microsoft.EntityFrameworkCore; -using Respawn.Graph; - -namespace EdFi.Ods.AdminApi.DBTests; +using Respawn; + +namespace EdFi.Ods.AdminApi.V1.DBTests; [TestFixture] -public abstract class PlatformSecurityContextTestBase53 +public abstract class PlatformSecurityContextTestBase { protected SqlServerSecurityContext TestContext { get; private set; } - protected SqlServerSecurityContext SetupContext { get; private set; } protected enum CheckpointPolicyOptions { @@ -28,39 +25,32 @@ protected enum CheckpointPolicyOptions } protected CheckpointPolicyOptions CheckpointPolicy { get; set; } = CheckpointPolicyOptions.BeforeEachTest; - - private Respawner _checkpoint; + + private readonly Checkpoint _checkpoint = new() + { + TablesToIgnore = + [ + "__MigrationHistory", "DeployJournal", "AdminApiDeployJournal" + ], + SchemasToExclude = [] + }; protected virtual string ConnectionString => TestContext.Database.GetConnectionString(); protected virtual void AdditionalFixtureSetup() { - } - - protected virtual async void CreateCheckpoint() - { - _checkpoint = await Respawner.CreateAsync(ConnectionString, new RespawnerOptions - { - TablesToIgnore = new Table[] - { - "__MigrationHistory", "DeployJournal", "AdminApiDeployJournal" - }, - SchemasToExclude = Array.Empty() - }); } protected abstract SqlServerSecurityContext CreateDbContext(); [OneTimeSetUp] public virtual async Task FixtureSetup() - { - CreateCheckpoint(); + { TestContext = CreateDbContext(); - SetupContext = CreateDbContext(); if (CheckpointPolicy == CheckpointPolicyOptions.BeforeAnyTest) { - await _checkpoint.ResetAsync(ConnectionString); + await _checkpoint.Reset(ConnectionString); } AdditionalFixtureSetup(); @@ -69,21 +59,22 @@ public virtual async Task FixtureSetup() [OneTimeTearDown] public async Task FixtureTearDown() { - await _checkpoint.ResetAsync(ConnectionString); - TestContext.Dispose(); - SetupContext.Dispose(); + await _checkpoint.Reset(ConnectionString); + if (TestContext != null) + { + TestContext.Dispose(); + TestContext = null; + } } [SetUp] public async Task SetUp() - { - CreateCheckpoint(); + { TestContext = CreateDbContext(); - SetupContext = CreateDbContext(); if (CheckpointPolicy == CheckpointPolicyOptions.BeforeEachTest) { - await _checkpoint.ResetAsync(ConnectionString); + await _checkpoint.Reset(ConnectionString); } } @@ -91,13 +82,12 @@ public async Task SetUp() public void TearDown() { TestContext.Dispose(); - SetupContext.Dispose(); } protected void Save(params object[] entities) { foreach (var entity in entities) - { + { TestContext.Add(entity); } @@ -126,26 +116,23 @@ protected TResult UsersTransaction(Func query) } protected void Transaction(Action action) - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlServer(Testing.SecurityV53ConnectionString); - - using var usersContext = new SqlServerSecurityContext(optionsBuilder.Options); - using var usersTransaction = usersContext.Database.BeginTransaction(); - action(usersContext); + { + using var transaction = TestContext.Database.BeginTransaction(); + action(TestContext); TestContext.SaveChanges(); - usersTransaction.Commit(); + transaction.Commit(); } protected TResult Transaction(Func query) { var result = default(TResult); - Transaction((usersContext) => + Transaction(database => { - result = query(usersContext); + result = query(database); }); return result; } + } diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/PlatformUsersContextTestBase.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/PlatformUsersContextTestBase.cs new file mode 100644 index 000000000..6b9f03c4a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/PlatformUsersContextTestBase.cs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Threading.Tasks; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using Respawn; +using static EdFi.Ods.AdminApi.V1.DBTests.Testing; + +namespace EdFi.Ods.AdminApi.V1.DBTests; + +[TestFixture] +public abstract class PlatformUsersContextTestBase +{ + private readonly Checkpoint _checkpoint = new() + { + TablesToIgnore = + [ + "__MigrationHistory", "DeployJournal", "AdminApiDeployJournal" + ], + SchemasToExclude = [] + }; + + protected static string ConnectionString => AdminConnectionString; + + [OneTimeTearDown] + public async Task FixtureTearDown() + { + await _checkpoint.Reset(ConnectionString); + } + + [SetUp] + public async Task SetUp() + { + await _checkpoint.Reset(ConnectionString); + } + + protected static void Save(params object[] entities) + { + Transaction(usersContext => + { + foreach (var entity in entities) + { + ((SqlServerUsersContext)usersContext).Add(entity); + } + }); + } + + protected static void Transaction(Action action) + { + using var usersContext = new SqlServerUsersContext(GetDbContextOptions()); + using var transaction = (usersContext).Database.BeginTransaction(); + action(usersContext); + usersContext.SaveChanges(); + transaction.Commit(); + } + + protected static TResult Transaction(Func query) + { + var result = default(TResult); + + Transaction(database => + { + result = query(database); + }); + + return result; + } + + protected static DbContextOptions GetDbContextOptions() + { + var builder = new DbContextOptionsBuilder(); + builder.UseSqlServer(ConnectionString); + return builder.Options; + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/SecurityData53TestBase.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/SecurityDataTestBase.cs similarity index 53% rename from Application/EdFi.Ods.AdminApi.DBTests/SecurityData53TestBase.cs rename to Application/EdFi.Ods.AdminApi.V1.DBTests/SecurityDataTestBase.cs index 787dfea11..a8f3e95f2 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/SecurityData53TestBase.cs +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/SecurityDataTestBase.cs @@ -2,33 +2,29 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; using System; using System.Collections.Generic; using System.Linq; using AutoMapper; -using EdFi.Ods.AdminApi.Infrastructure; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.AutoMapper; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; using NUnit.Framework; -using Action = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.Action; -using ActionName = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.Action; -using ClaimSetEditorTypes = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using Microsoft.EntityFrameworkCore; - -namespace EdFi.Ods.AdminApi.DBTests; +using Action = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Action; +using ActionName = EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor.Action; +using ClaimSetEditorTypes = EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +namespace EdFi.Ods.AdminApi.V1.DBTests; [TestFixture] -public abstract class SecurityData53TestBase : PlatformSecurityContextTestBase53 +public abstract class SecurityDataTestBase : PlatformSecurityContextTestBase { - protected override string ConnectionString => Testing.SecurityV53ConnectionString; + protected override string ConnectionString => Testing.SecurityConnectionString; protected override SqlServerSecurityContext CreateDbContext() - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlServer(ConnectionString); - return new SqlServerSecurityContext(optionsBuilder.Options); + { + return new SqlServerSecurityContext(Testing.GetDbContextOptions(ConnectionString)); } // This bool controls whether or not to run SecurityContext initialization @@ -36,6 +32,14 @@ protected override SqlServerSecurityContext CreateDbContext() // inserted into the security database on fixture setup. protected bool SeedSecurityContextOnFixtureSetup { get; set; } = false; + protected override void AdditionalFixtureSetup() + { + if (SeedSecurityContextOnFixtureSetup) + { + TestContext.Database.EnsureCreated(); + } + } + protected void LoadSeedData() { var odsApplication = GetOrCreateApplication("Ed-Fi ODS API"); @@ -66,39 +70,55 @@ protected void LoadSeedData() Application GetOrCreateApplication(string applicationName) { - var application = TestContext.Applications.FirstOrDefault(a => a.ApplicationName == applicationName) ?? - TestContext.Applications.Add(new Application - { - ApplicationName = "Ed-Fi ODS API" - }).Entity; + var application = + TestContext.Applications.FirstOrDefault(a => a.ApplicationName == applicationName); + if (application == null) + { + application = new Application + { + ApplicationName = "Ed-Fi ODS API" + }; + TestContext.Applications.Add(application); + } return application; } Action GetOrCreateAction(string actionName) { - var action = TestContext.Actions.FirstOrDefault(a => a.ActionName == actionName) ?? - TestContext.Actions.Add(new Action - { - ActionName = actionName, - ActionUri = $"http://ed-fi.org/odsapi/actions/{actionName}" - }).Entity; + var action = TestContext.Actions.FirstOrDefault(a => a.ActionName == actionName); + if (action == null) + { + action = new Action + { + ActionName = actionName, + ActionUri = $"http://ed-fi.org/odsapi/actions/{actionName}" + }; + TestContext.Actions.Add(action); + } return action; } AuthorizationStrategy GetOrCreateAuthorizationStrategy(Application application, string displayName, string authorizationStrategyName) { - var authorizationStrategy = TestContext.AuthorizationStrategies.FirstOrDefault(a => - a.Application.ApplicationId == application.ApplicationId && a.DisplayName == displayName && - a.AuthorizationStrategyName == authorizationStrategyName) ?? - TestContext.AuthorizationStrategies.Add( - new AuthorizationStrategy - { - DisplayName = displayName, - AuthorizationStrategyName = authorizationStrategyName, - Application = application - }).Entity; + var authorizationStrategy = TestContext.AuthorizationStrategies.FirstOrDefault( + a => + a.Application.ApplicationId == application.ApplicationId && + a.DisplayName == displayName && + a.AuthorizationStrategyName == authorizationStrategyName); + + if (authorizationStrategy == null) + { + authorizationStrategy = + new AuthorizationStrategy + { + DisplayName = displayName, + AuthorizationStrategyName = authorizationStrategyName, + Application = application + }; + TestContext.AuthorizationStrategies.Add(authorizationStrategy); + } return authorizationStrategy; } @@ -106,34 +126,47 @@ AuthorizationStrategy GetOrCreateAuthorizationStrategy(Application application, ResourceClaim GetOrCreateResourceClaim(string resourceName, Application application) { var resourceClaim = - TestContext.ResourceClaims.FirstOrDefault(r => - r.ResourceName == resourceName && r.Application.ApplicationId == application.ApplicationId) ?? - TestContext.ResourceClaims.Add(new ResourceClaim + TestContext.ResourceClaims.FirstOrDefault( + r => + r.ResourceName == resourceName && + r.Application.ApplicationId == application.ApplicationId); + + if (resourceClaim == null) + { + resourceClaim = new ResourceClaim { Application = application, DisplayName = resourceName, ResourceName = resourceName, ClaimName = $"http://ed-fi.org/ods/identity/claims/domains/{resourceName}", ParentResourceClaim = null - }).Entity; - + }; + TestContext.ResourceClaims.Add(resourceClaim); + } return resourceClaim; } void GetOrCreateResourceClaimAuthorizationMetadata(Action action, - AuthorizationStrategy authorizationStrategy, - ResourceClaim resourceClaim) + AuthorizationStrategy authorizationStrategy, + ResourceClaim resourceClaim) { - var resourceClaimAuthorizationMetadata = TestContext.ResourceClaimAuthorizationMetadatas.FirstOrDefault(rcm => - rcm.Action.ActionId == action.ActionId && rcm.AuthorizationStrategy.AuthorizationStrategyId == authorizationStrategy.AuthorizationStrategyId && + var resourceClaimAuthorizationMetadata = TestContext.ResourceClaimActions.FirstOrDefault(rcm => + rcm.Action.ActionId == action.ActionId && rcm.AuthorizationStrategies.FirstOrDefault() + .AuthorizationStrategyId == authorizationStrategy.AuthorizationStrategyId && rcm.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId); if (resourceClaimAuthorizationMetadata == null) { - TestContext.ResourceClaimAuthorizationMetadatas.Add(new ResourceClaimAuthorizationMetadata + TestContext.ResourceClaimActions.Add(new ResourceClaimAction { Action = action, - AuthorizationStrategy = authorizationStrategy, + AuthorizationStrategies = authorizationStrategy != null ? + [ + new() { + AuthorizationStrategy = authorizationStrategy, + ResourceClaimAction = resourceClaimAuthorizationMetadata + } + ] : null, ResourceClaim = resourceClaim, ValidationRuleSetName = null }); @@ -141,31 +174,35 @@ void GetOrCreateResourceClaimAuthorizationMetadata(Action action, } } - protected IReadOnlyCollection SetupResourceClaims(Application testApplication, int resourceClaimCount = 5, int childResourceClaimCount = 3) + protected IReadOnlyCollection SetupResourceClaims(Application testApplication, IList parentRcNames, IList childRcNames) { var parentResourceClaims = new List(); var childResourceClaims = new List(); var actions = new List(); - foreach (var parentIndex in Enumerable.Range(1, resourceClaimCount)) + foreach (var parentName in parentRcNames) { var resourceClaim = new ResourceClaim { - ClaimName = $"TestParentResourceClaim{parentIndex}", - DisplayName = $"TestParentResourceClaim{parentIndex}", - ResourceName = $"TestParentResourceClaim{parentIndex}", + ClaimName = parentName, + DisplayName = parentName, + ResourceName = parentName, Application = testApplication }; parentResourceClaims.Add(resourceClaim); - childResourceClaims.AddRange(Enumerable.Range(1, childResourceClaimCount) - .Select(childIndex => new ResourceClaim + childResourceClaims.AddRange(childRcNames + .Select(childName => { - ClaimName = $"TestChildResourceClaim{childIndex}", - DisplayName = $"TestChildResourceClaim{childIndex}", - ResourceName = $"TestChildResourceClaim{childIndex}", - Application = testApplication, - ParentResourceClaim = resourceClaim, - ParentResourceClaimId = resourceClaim.ResourceClaimId + var childRcName = $"{childName}-{parentName}"; + return new ResourceClaim + { + ClaimName = childRcName, + DisplayName = childRcName, + ResourceName = childRcName, + Application = testApplication, + ParentResourceClaim = resourceClaim, + ParentResourceClaimId = resourceClaim.ResourceClaimId + }; })); } @@ -186,40 +223,60 @@ protected IReadOnlyCollection SetupResourceClaims(Application tes return parentResourceClaims; } - protected IReadOnlyCollection SetupParentResourceClaimsWithChildren(ClaimSet testClaimSet, Application testApplication, int resourceClaimCount = 5, int childResourceClaimCount = 3) + public static IList UniqueNameList(string prefix, int resourceClaimCount = 5) + { + var random = new Random(); + var parentResourceClaims = Enumerable.Range(1, resourceClaimCount).Select(index => + { + return $"{prefix}{random.Next()}"; + }).ToList(); + + return parentResourceClaims; + } + + protected IReadOnlyCollection SetupParentResourceClaimsWithChildren(ClaimSet testClaimSet, Application testApplication, IList parentRcNames, IList childRcNames) { var actions = ActionName.GetAll().Select(action => new Action { ActionName = action.Value, ActionUri = action.Value }).ToList(); Save(actions.Cast().ToArray()); - var parentResourceClaims = Enumerable.Range(1, resourceClaimCount).Select(parentIndex => new ResourceClaim + var parentResourceClaims = parentRcNames.Select(parentRcName => { - ClaimName = $"TestParentResourceClaim{parentIndex}", - DisplayName = $"TestParentResourceClaim{parentIndex}", - ResourceName = $"TestParentResourceClaim{parentIndex}", Application = testApplication + return new ResourceClaim + { + ClaimName = parentRcName, + DisplayName = parentRcName, + ResourceName = parentRcName, + Application = testApplication + }; }).ToList(); - var childResourceClaims = parentResourceClaims.SelectMany(x => Enumerable.Range(1, childResourceClaimCount) - .Select(childIndex => new ResourceClaim - { - ClaimName = $"TestChildResourceClaim{childIndex}", - DisplayName = $"TestChildResourceClaim{childIndex}", - ResourceName = $"TestChildResourceClaim{childIndex}", - Application = testApplication, - ParentResourceClaim = x - })).ToList(); + var childResourceClaims = parentResourceClaims.SelectMany(x => childRcNames + .Select(childRcName => + { + var childName = $"{childRcName}-{x.ClaimName}"; + return new ResourceClaim + { + ClaimName = childName, + DisplayName = childName, + ResourceName = childName, + Application = testApplication, + ParentResourceClaim = x + }; + })).ToList(); Save(childResourceClaims.Cast().ToArray()); - var claimSetResourceClaims = Enumerable.Range(1, resourceClaimCount) - .Select(index => parentResourceClaims[index - 1]).Select(parentResource => new ClaimSetResourceClaim + var claimSetResourceClaims = Enumerable.Range(1, parentRcNames.Count) + .Select(index => parentResourceClaims[index - 1]).Select(parentResource => new ClaimSetResourceClaimAction { ResourceClaim = parentResource, - Action = actions.Single(x => x.ActionName == ActionName.Create.Value), ClaimSet = testClaimSet + Action = actions.Single(x => x.ActionName == ActionName.Create.Value), + ClaimSet = testClaimSet }).ToList(); var childResources = parentResourceClaims.SelectMany(x => childResourceClaims .Where(child => child.ParentResourceClaimId == x.ResourceClaimId) - .Select(child => new ClaimSetResourceClaim + .Select(child => new ClaimSetResourceClaimAction { ResourceClaim = child, Action = actions.Single(a => a.ActionName == ActionName.Create.Value), @@ -253,20 +310,25 @@ protected IReadOnlyCollection SetupApplicationAuthorizati return authStrategies; } - protected IReadOnlyCollection SetupResourcesWithDefaultAuthorizationStrategies(List testAuthorizationStrategies, List claimSetResourceClaims) + protected IReadOnlyCollection SetupResourcesWithDefaultAuthorizationStrategies(List testAuthorizationStrategies, List claimSetResourceClaims) { - var resourceClaimWithDefaultAuthStrategies = new List(); + var resourceClaimWithDefaultAuthStrategies = new List(); var random = new Random(); foreach (var resourceClaim in claimSetResourceClaims) { var testAuthorizationStrategy = testAuthorizationStrategies[random.Next(testAuthorizationStrategies.Count)]; - var resourceClaimWithDefaultAuthStrategy = new ResourceClaimAuthorizationMetadata + var resourceClaimWithDefaultAuthStrategy = new ResourceClaimAction { ResourceClaim = resourceClaim.ResourceClaim, Action = resourceClaim.Action, - AuthorizationStrategy = testAuthorizationStrategy }; + + var rcActionAuthorizationStrategies = testAuthorizationStrategy != null ? + new List { + new() { AuthorizationStrategy = testAuthorizationStrategy, ResourceClaimAction = resourceClaimWithDefaultAuthStrategy } } : null; + + resourceClaimWithDefaultAuthStrategy.AuthorizationStrategies = rcActionAuthorizationStrategies; resourceClaimWithDefaultAuthStrategies.Add(resourceClaimWithDefaultAuthStrategy); } @@ -275,16 +337,15 @@ protected IReadOnlyCollection SetupResources return resourceClaimWithDefaultAuthStrategies; } - private static IMapper Mapper() => new MapperConfiguration(cfg => cfg.AddProfile()).CreateMapper(); + protected static IMapper Mapper() => new MapperConfiguration(cfg => cfg.AddProfile()).CreateMapper(); protected List ResourceClaimsForClaimSet(int securityContextClaimSetId) { List list = null; using (var securityContext = CreateDbContext()) { - var getResourcesByClaimSetIdQuery = new ClaimSetEditorTypes.GetResourcesByClaimSetIdQuery(new StubOdsSecurityModelVersionResolver.V3_5(), - new ClaimSetEditorTypes.GetResourcesByClaimSetIdQueryV53Service(securityContext, Mapper()), null); - list = getResourcesByClaimSetIdQuery.AllResources(securityContextClaimSetId).ToList(); + var getResourcesByClaimSetIdQuery = new ClaimSetEditorTypes.GetResourcesByClaimSetIdQuery(new ClaimSetEditorTypes.GetResourcesByClaimSetIdQueryService(securityContext, Mapper())); + list = [.. getResourcesByClaimSetIdQuery.AllResources(securityContextClaimSetId)]; } return list; } @@ -294,8 +355,7 @@ protected ClaimSetEditorTypes.ResourceClaim SingleResourceClaimForClaimSet(int s ClaimSetEditorTypes.ResourceClaim resourceClaim = null; using (var securityContext = CreateDbContext()) { - var getResourcesByClaimSetIdQuery = new ClaimSetEditorTypes.GetResourcesByClaimSetIdQuery(new StubOdsSecurityModelVersionResolver.V3_5(), - new ClaimSetEditorTypes.GetResourcesByClaimSetIdQueryV53Service(securityContext, Mapper()), null); + var getResourcesByClaimSetIdQuery = new ClaimSetEditorTypes.GetResourcesByClaimSetIdQuery(new ClaimSetEditorTypes.GetResourcesByClaimSetIdQueryService(securityContext, Mapper())); resourceClaim = getResourcesByClaimSetIdQuery.SingleResource(securityContextClaimSetId, resourceClaimId); } return resourceClaim; diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/Testing.cs b/Application/EdFi.Ods.AdminApi.V1.DBTests/Testing.cs new file mode 100644 index 000000000..6532f085a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/Testing.cs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Settings; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.V1.DBTests; + +public static class Testing +{ + private static IConfigurationRoot _config; + + public static IConfiguration Configuration() + { + _config ??= new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables() + .Build(); + return _config; + } + + public static string AdminConnectionString { get { return Configuration().GetConnectionString("EdFi_Admin"); } } + + public static string SecurityConnectionString { get { return Configuration().GetConnectionString("EdFi_Security"); } } + + public static int DefaultPageSizeOffset => (int)Configuration().GetValue(typeof(int), "AppSettings:DefaultPageSizeOffset"); + + public static int DefaultPageSizeLimit => (int)Configuration().GetValue(typeof(int), "AppSettings:DefaultPageSizeLimit"); + + public static DbContextOptions GetDbContextOptions(string connectionString) + { + var builder = new DbContextOptionsBuilder(); + builder.UseSqlServer(connectionString); + return builder.Options; + } + + public static IOptions GetAppSettings() + { + AppSettings appSettings = new() + { + DefaultPageSizeOffset = DefaultPageSizeOffset, + DefaultPageSizeLimit = DefaultPageSizeLimit + }; + IOptions options = Options.Create(appSettings); + return options; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1.DBTests/appsettings.json b/Application/EdFi.Ods.AdminApi.V1.DBTests/appsettings.json new file mode 100644 index 000000000..83117ecac --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1.DBTests/appsettings.json @@ -0,0 +1,15 @@ +{ + "AppSettings": { + "AppStartup": "OnPrem", + "ApiStartupType": "sandbox", + "XsdFolder": "Schema", + "DatabaseEngine": "SqlServer", + "EncryptionKey": "bEnFYNociET2R1Wua3DHzwfU5u/Fa47N5fw0PXD0OSI=", + "DefaultPageSizeOffset": 0, + "DefaultPageSizeLimit": 25 + }, + "ConnectionStrings": { + "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", + "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/IUsersContext.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/IUsersContext.cs new file mode 100644 index 000000000..6d1c10fa4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/IUsersContext.cs @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts +{ + public interface IUsersContext : IDisposable + { + DbSet Users { get; set; } + + DbSet Clients { get; set; } + + DbSet ClientAccessTokens { get; set; } + + DbSet Vendors { get; set; } + + DbSet Applications { get; set; } + + DbSet Profiles { get; set; } + + DbSet OdsInstances { get; set; } + + DbSet OdsInstanceComponents { get; set; } + + DbSet ApplicationEducationOrganizations { get; set; } + + DbSet VendorNamespacePrefixes { get; set; } + + DbSet OwnershipTokens { get; set; } + + DbSet ApiClientOwnershipTokens { get; set; } + + int SaveChanges(); + + Task SaveChangesAsync(CancellationToken cancellationToken); + + /// + /// Asynchronously executes a raw SQL statement with only a scalar result (e.g. row count). + /// + /// Query to execute, optionally containing parameter strings using @ symbol token + /// Optional parameters + /// Statement result (row count by default) + /// + /// + /// No parameters: + /// + /// + /// result = await _usersContext.ExecuteQueryAsync(sqlCommand); + /// ]]> + /// + /// + /// Single parameter, @p0. The name does not matter; @p0 is a good standard to follow to indicate to the reader that it is the first parameter. + /// + /// + /// @p0"; + /// IReadOnlyList result = await _usersContext.ExecuteQueryAsync(sqlCommand, fieldTwoLowerLimit); + /// ]]> + /// + /// + /// Two parameters, second one for a CreateDate field that we're not going to return. + /// + /// + /// @p0 AND CreateDate < '@p1'"; + /// IReadOnlyList result = await _usersContext.ExecuteQueryAsync(sqlCommand, fieldTwoLowerLimit, createDateUpperLimit); + /// ]]> + /// + /// + Task ExecuteSqlCommandAsync(string sqlStatement, params object[] parameters); + + /// + /// Asynchronously executes a raw SQL query and maps the results to an object of type . + /// + /// Any class with properties matching the column names in the query + /// Query to execute, optionally containing parameter strings using @ symbol token + /// Optional parameters + /// Readonly list of + /// + /// + /// Given this return entity: + /// + /// + /// public class Something { public string FieldOne { get; set; } public int FieldTwo { get; set; } } + /// + /// + /// No parameters: + /// + /// + /// result = await _usersContext.ExecuteQueryAsync(sqlCommand); + /// ]]> + /// + /// + /// Single parameter, @p0. The name does not matter; @p0 is a good standard to follow to indicate to the reader that it is the first parameter. + /// + /// + /// @p0"; + /// IReadOnlyList result = await _usersContext.ExecuteQueryAsync(sqlCommand, fieldTwoLowerLimit); + /// ]]> + /// + /// + /// Two parameters, second one for a CreateDate field that we're not going to return. + /// + /// + /// @p0 AND CreateDate < '@p1'"; + /// IReadOnlyList result = await _usersContext.ExecuteQueryAsync(sqlCommand, fieldTwoLowerLimit, createDateUpperLimit); + /// ]]> + /// + /// + Task> ExecuteQueryAsync(string sqlStatement, params object[] parameters); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/IUsersContextFactory.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/IUsersContextFactory.cs new file mode 100644 index 000000000..a1b5e0ba4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/IUsersContextFactory.cs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts +{ + public interface IUsersContextFactory + { + IUsersContext CreateContext(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/PostgresUsersContext.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/PostgresUsersContext.cs new file mode 100644 index 000000000..576e20efa --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/PostgresUsersContext.cs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common.Utils.Extensions; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Extensions; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts +{ + public class PostgresUsersContext : UsersContext + { + public PostgresUsersContext(DbContextOptions options) + : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Model.GetEntityTypes().ForEach( + entityType => + entityType.SetSchema("dbo")); + + modelBuilder.Model.GetEntityTypes().Single(e => e.ClrType.Name == nameof(ApiClientApplicationEducationOrganization)) + .GetProperty("ApplicationEducationOrganizationId") + .SetColumnName("applicationedorg_applicationedorgid"); + + modelBuilder.MakeDbObjectNamesLowercase(); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/SqlServerUsersContext.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/SqlServerUsersContext.cs new file mode 100644 index 000000000..6925e2595 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/SqlServerUsersContext.cs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts +{ + public class SqlServerUsersContext : UsersContext + { + public SqlServerUsersContext(DbContextOptions options) : base(options) { } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/UsersContext.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/UsersContext.cs new file mode 100644 index 000000000..49cb6c7c9 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/UsersContext.cs @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Extensions; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts +{ + public abstract class UsersContext : DbContext, IUsersContext + { + + protected UsersContext(DbContextOptions options) + : base(options) { } + + public const string UserTableName = "Users"; + + public static string UserNameColumn + { + get { return UserMemberName(x => x.Email); } + } + + public static string UserIdColumn + { + get { return UserMemberName(x => x.UserId); } + } + + public DbSet Users { get; set; } + + public DbSet Clients { get; set; } + + public DbSet ClientAccessTokens { get; set; } + + public DbSet Vendors { get; set; } + + public DbSet Applications { get; set; } + + public DbSet Profiles { get; set; } + + public DbSet OdsInstances { get; set; } + + public DbSet OdsInstanceComponents { get; set; } + + //TODO: This should really be removed from being directly on the context. Application should own + //TODO: these instances, and deleting an application should delete the associated LEA's + public DbSet ApplicationEducationOrganizations { get; set; } + + public DbSet VendorNamespacePrefixes { get; set; } + + public DbSet OwnershipTokens { get; set; } + + public DbSet ApiClientOwnershipTokens { get; set; } + + public DbSet UsersInRoles { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasMany(t => t.ApplicationEducationOrganizations) + .WithMany(t => t.Clients) + .UsingEntity( + "ApiClientApplicationEducationOrganizations", + l => + l.HasOne().WithMany().HasForeignKey( + "ApplicationEducationOrganizationId"), + r => + r.HasOne().WithMany().HasForeignKey("ApiClientId")); + + modelBuilder.Entity() + .HasMany(a => a.Profiles) + .WithMany(a => a.Applications) + .UsingEntity("ProfileApplications"); + + modelBuilder.UseUnderscoredFkColumnNames(); + + modelBuilder.Model.FindEntityTypes(typeof(ApiClient)).First().GetProperty("CreatorOwnershipTokenId") + .SetColumnName("CreatorOwnershipTokenId_OwnershipTokenId"); + } + + /// + public Task ExecuteSqlCommandAsync(string sqlStatement, params object[] parameters) + { + return Database.ExecuteSqlInterpolatedAsync( + FormattableStringFactory.Create(sqlStatement.ToLowerInvariant(), parameters)); + } + + /// + public async Task> ExecuteQueryAsync(string sqlStatement, params object[] parameters) + { + return await Database + .SqlQueryRaw(sqlStatement.ToLowerInvariant(), parameters) + .ToListAsync(); + } + + private static string UserMemberName(Expression> emailExpression) + { + return MemberName(emailExpression); + + string MemberName(LambdaExpression expression) + { + var memberExpression = expression.Body as MemberExpression; + + if (memberExpression != null) + { + return memberExpression.Member.Name; + } + + var methodExpression = expression.Body as MethodCallExpression; + + if (methodExpression != null) + { + return methodExpression.Method.Name; + } + + var unaryExpression = expression.Body as UnaryExpression; + + if (unaryExpression != null) + { + var unaryMember = unaryExpression.Operand as MemberExpression; + + if (unaryMember == null) + { + throw new ArgumentException($"Strange operand in unary expression '{expression}'"); + } + + return unaryMember.Member.Name; + } + + throw new ArgumentException($"Expression '{expression}' of type '{expression.Body.GetType()}' is not handled"); + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/UsersContextFactory.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/UsersContextFactory.cs new file mode 100644 index 000000000..92846e643 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Contexts/UsersContextFactory.cs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common; +using EdFi.Common.Configuration; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Providers; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts +{ + public class UsersContextFactory : IUsersContextFactory + { + private readonly Dictionary _usersContextTypeByDatabaseEngine = new() + { + {DatabaseEngine.SqlServer, typeof(SqlServerUsersContext)}, + {DatabaseEngine.Postgres, typeof(PostgresUsersContext)} + }; + + private readonly DatabaseEngine _databaseEngine; + + private readonly IAdminDatabaseConnectionStringProvider _connectionStringsProvider; + + public UsersContextFactory(IAdminDatabaseConnectionStringProvider connectionStringsProvider, DatabaseEngine databaseEngine) + { + _connectionStringsProvider = Preconditions.ThrowIfNull(connectionStringsProvider, nameof(connectionStringsProvider)); + _databaseEngine = Preconditions.ThrowIfNull(databaseEngine, nameof(databaseEngine)); + } + + public Type GetUsersContextType() + { + if (_usersContextTypeByDatabaseEngine.TryGetValue(_databaseEngine, out Type? contextType) && contextType != null) + { + return contextType; + } + + throw new InvalidOperationException( + $"No UsersContext defined for database type {_databaseEngine.DisplayName}"); + } + + public IUsersContext CreateContext() + { + if (_databaseEngine == DatabaseEngine.SqlServer) + { + return Activator.CreateInstance( + GetUsersContextType(), + new DbContextOptionsBuilder() + .UseLazyLoadingProxies() + .UseSqlServer(_connectionStringsProvider.GetConnectionString()) + .Options) as + IUsersContext ?? throw new InvalidOperationException("Failed to create SqlServerUsersContext instance."); + } + + if (_databaseEngine == DatabaseEngine.Postgres) + { + return Activator.CreateInstance( + GetUsersContextType(), + new DbContextOptionsBuilder() + .UseLazyLoadingProxies() + .UseNpgsql(_connectionStringsProvider.GetConnectionString()) + .Options) as + IUsersContext ?? throw new InvalidOperationException("Failed to create PostgresUsersContext instance."); + } + + throw new InvalidOperationException( + $"Cannot create an IUsersContext for database type {_databaseEngine.DisplayName}"); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Extensions/DbContextExtensions.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Extensions/DbContextExtensions.cs new file mode 100644 index 000000000..fe0399a4b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Extensions/DbContextExtensions.cs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Extensions +{ + public static class DbContextExtensions + { + public static void DeleteAll(this DbContext context) where T : class + { + foreach (var p in context.Set().ToList()) + { + context.Entry(p).State = EntityState.Deleted; + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Extensions/ModelBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Extensions/ModelBuilderExtensions.cs new file mode 100644 index 000000000..0f9892a57 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Extensions/ModelBuilderExtensions.cs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Extensions; + +public static class ModelBuilderExtensions +{ + public static void MakeDbObjectNamesLowercase(this ModelBuilder modelBuilder) + { + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + if (entityType.GetTableName() is not { } tableName) + { + continue; + } + + entityType.SetTableName(tableName.ToLowerInvariant()); + + foreach (var property in entityType.GetProperties()) + { + property.SetColumnName(property.GetColumnName().ToLowerInvariant()); + } + } + } + + // EF Core does not recognize the _ convention + // for FK column names, so we update column names in the model metadata to match the database + public static void UseUnderscoredFkColumnNames(this ModelBuilder modelBuilder) + { + foreach (var foreignKey in modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys())) + { + foreach (IMutableProperty foreignKeyProperty in foreignKey.Properties) + { + foreignKeyProperty.SetColumnName( + $"{foreignKey.GetNavigation(true)?.TargetEntityType.ShortName() ?? foreignKey.PrincipalKey.DeclaringEntityType.ShortName()}_{foreignKey.PrincipalKey.Properties.Single().Name}"); + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/AccountModels.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/AccountModels.cs new file mode 100644 index 000000000..6427d1e11 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/AccountModels.cs @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + public class User + { + public User() + { + ApiClients = []; + } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int UserId { get; set; } + + public required string Email { get; set; } + + public required string FullName { get; set; } + + public required Vendor Vendor { get; set; } + + public virtual ICollection ApiClients { get; set; } + + public ApiClient AddSandboxClient(string name, SandboxType sandboxType, string key, string secret) + { + var client = new ApiClient(true) + { + Name = name, + IsApproved = true, + UseSandbox = true, + SandboxType = sandboxType + }; + + if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(secret)) + { + client.Key = key; + client.Secret = secret; + } + + ApiClients.Add(client); + return client; + } + + public static User Create(string userEmail, string userName, Vendor vendor) + { + return new User + { + Email = userEmail, + FullName = userName, + Vendor = vendor + }; + } + } + + public class RegisterExternalLoginModel + { + [Required] + [Display(Name = "User name")] + public string UserName { get; set; } = string.Empty; + + public string ExternalLoginData { get; set; } = string.Empty; + } + + public class LocalPasswordModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } = string.Empty; + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } = string.Empty; + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } = string.Empty; + } + + public class LoginModel + { + [Required] + [Display(Name = "Email Address")] + public required string EmailAddress { get; set; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public required string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } + + public class RegisterModel + { + [Required] + [Display(Name = "User name")] + public required string UserName { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public required string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public required string ConfirmPassword { get; set; } + } + + public class ExternalLogin + { + public string Provider { get; set; } = string.Empty; + + public string ProviderDisplayName { get; set; } = string.Empty; + + public string ProviderUserId { get; set; } = string.Empty; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ApiClient.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ApiClient.cs new file mode 100644 index 000000000..07464b393 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ApiClient.cs @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Security.Cryptography; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + /// + /// Class representing EdFi client application information persisted in a data store. + /// A Client has a list of domains that are valid for access + /// + [Table("ApiClients")] + public class ApiClient + { + public ApiClient() + { + ClientAccessTokens = new List(); + ApplicationEducationOrganizations = new Collection(); + Domains = new Dictionary(); + } + + public ApiClient(bool generateKey = false) + : this() + { + if (!generateKey) + { + return; + } + + Key = GenerateRandomString(12); + Secret = GenerateRandomString(); + } + + public int ApiClientId { get; set; } + + [Required] + [StringLength(50)] + public string Key { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + public string Secret { get; set; } = string.Empty; + + public bool SecretIsHashed { get; set; } + + [Required] + [StringLength(50)] + public required string Name { get; set; } = string.Empty; + + public bool IsApproved { get; set; } + + public bool UseSandbox { get; set; } + + public SandboxType SandboxType { get; set; } + + public string SandboxTypeName => SandboxType.ToString(); + + [NotMapped] + public string Status { get; set; } = string.Empty; + + public string KeyStatus { get; set; } = string.Empty; + + public string ChallengeId { get; set; } = string.Empty; + + public DateTime? ChallengeExpiry { get; set; } + + public string ActivationCode { get; set; } = string.Empty; + + public int? ActivationRetried { get; set; } + + public virtual OwnershipToken? CreatorOwnershipToken { get; set; } + + [Column("CreatorOwnershipTokenId_OwnershipTokenId")] + public short? CreatorOwnershipTokenId { get; set; } + + [StringLength(306)] + public string StudentIdentificationSystemDescriptor { get; set; } = string.Empty; + + public virtual Application? Application { get; set; } + + public virtual User? User { get; set; } + + public virtual ICollection ApplicationEducationOrganizations { get; set; } + + public virtual List ClientAccessTokens { get; set; } + + [NotMapped] + public Dictionary Domains { get; set; } + + private static string GenerateRandomString(int length = 24) + { + string result; + var numBytes = (length + 3) / 4 * 3; + var bytes = new byte[numBytes]; + + using (var rng = RandomNumberGenerator.Create()) + { + do + { + rng.GetBytes(bytes); + result = Convert.ToBase64String(bytes); + } + while (result.Contains("+") || result.Contains("/")); + } + + return result.Substring(0, length); + } + + public string GenerateSecret() + { + return Secret = GenerateRandomString(); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ApiClientApplicationEducationOrganization.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ApiClientApplicationEducationOrganization.cs new file mode 100644 index 000000000..30c6d745c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ApiClientApplicationEducationOrganization.cs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + [PrimaryKey("ApiClientId", "ApplicationEducationOrganizationId")] + [Index("ApiClientId")] + [Index("ApplicationEducationOrganizationId")] + public class ApiClientApplicationEducationOrganization + { + [Column("ApiClient_ApiClientId")] + public int ApiClientId { get; set; } + + [Column("ApplicationEducationOrganization_ApplicationEducationOrganizationId")] + public int ApplicationEducationOrganizationId { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ApiClientOwnershipToken.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ApiClientOwnershipToken.cs new file mode 100644 index 000000000..a7217cd8e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ApiClientOwnershipToken.cs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + /// + /// Class representing the assignment of one or more ownership tokens to an API client. + /// A API Client has a list of Ownership tokens. + /// + public class ApiClientOwnershipToken + { + /// + /// Numeric Identifier which is an Identity column which distinguish the uniques combination of ApiClient and Ownership Token + /// + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ApiClientOwnershipTokenId { get; set; } + + [Required] + public virtual ApiClient ApiClient { get; set; } = null!; + + [Required] + public virtual OwnershipToken OwnershipToken { get; set; } = null!; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/Application.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/Application.cs new file mode 100644 index 000000000..970a07f30 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/Application.cs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + public class Application + { + public Application() + { + ApplicationEducationOrganizations = []; + ApiClients = []; + Profiles = []; + OperationalContextUri = string.Empty; + } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ApplicationId { get; set; } + + public string? ApplicationName { get; set; } + + [StringLength(255)] + public string? ClaimSetName { get; set; } + + public virtual Vendor? Vendor { get; set; } + + public virtual OdsInstance? OdsInstance { get; set; } + + [StringLength(255)] + [Required] + public required string OperationalContextUri { get; set; } + + public virtual ICollection ApplicationEducationOrganizations { get; set; } + + public virtual ICollection ApiClients { get; set; } + + public virtual ICollection Profiles { get; set; } + + public ApplicationEducationOrganization CreateApplicationEducationOrganization(int educationOrganizationId) + => new() + { + EducationOrganizationId = educationOrganizationId, + Application = this, + Clients = ApiClients + }; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ApplicationEducationOrganization.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ApplicationEducationOrganization.cs new file mode 100644 index 000000000..b9dfc79ff --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ApplicationEducationOrganization.cs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + public class ApplicationEducationOrganization + { + public ApplicationEducationOrganization() + { + Clients = new Collection(); + } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ApplicationEducationOrganizationId { get; set; } + + public virtual Application? Application { get; set; } + + public int EducationOrganizationId { get; set; } + + public virtual ICollection Clients { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ClientAccessToken.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ClientAccessToken.cs new file mode 100644 index 000000000..96ff8d854 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/ClientAccessToken.cs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + public class ClientAccessToken + { + private Guid _id; + + //Does not instantiate GUID for case of loading from database + public ClientAccessToken() { } + + //For creation of a new Token + public ClientAccessToken(TimeSpan lifespan) + { + Expiration = DateTime.UtcNow.Add(lifespan); + Duration = lifespan; + } + + public Guid Id + { + get + { + if (_id == default(Guid)) + { + _id = Guid.NewGuid(); + } + + return _id; + } + set { _id = value; } + } + + public virtual ApiClient? ApiClient { get; set; } + + public DateTime Expiration { get; set; } + + [NotMapped] + public TimeSpan Duration { get; } + + public string? Scope { get; set; } + + public override string ToString() + { + return Id.ToString("N"); + } + + public bool IsExpired() + { + return DateTime.UtcNow.Subtract(Expiration) > TimeSpan.Zero; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/OAuthTokenClient.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/OAuthTokenClient.cs new file mode 100644 index 000000000..e1cc6d6bd --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/OAuthTokenClient.cs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + public class OAuthTokenClient + { + public string Key { get; set; } = string.Empty; + + public bool UseSandbox { get; set; } + + public string StudentIdentificationSystemDescriptor { get; set; } = string.Empty; + + public int? EducationOrganizationId { get; set; } + + public string ClaimSetName { get; set; } = string.Empty; + + public string NamespacePrefix { get; set; } = string.Empty; + + public string ProfileName { get; set; } = string.Empty; + + public short? CreatorOwnershipTokenId { get; set; } + + public short? OwnershipTokenId { get; set; } + + public DateTime Expiration { get; set; } + + public int ApiClientId { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/OdsInstance.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/OdsInstance.cs new file mode 100644 index 000000000..0ba2a2244 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/OdsInstance.cs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + public class OdsInstance + { + public OdsInstance() + { + OdsInstanceComponents = []; + } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int OdsInstanceId { get; set; } + + /// + /// Friendly name for the ODS Instance, to be displayed / set by the end-user + /// + [Required] + [StringLength(100)] + public required string Name { get; set; } + + /// + /// Type of ODS instance this identifies (e.g. "Enterprise" or "Cloud") + /// + [Required] + [StringLength(100)] + public required string InstanceType { get; set; } + + /// + /// Current status of this ODS instance, for display/use by management tooling + /// + [Required] + [StringLength(100)] + public required string Status { get; set; } + + /// + /// If set to true, signifies that this ODS installation has been extended with custom code + /// + public bool IsExtended { get; set; } + + /// + /// Version number of this ODS installation + /// + [Required] + [StringLength(20)] + public required string Version { get; set; } + + public virtual ICollection OdsInstanceComponents { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/OdsInstanceComponent.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/OdsInstanceComponent.cs new file mode 100644 index 000000000..cb16db3b5 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/OdsInstanceComponent.cs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + public class OdsInstanceComponent + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int OdsInstanceComponentId { get; set; } + + /// + /// The display name of this particular component, for display/use by management tooling + /// + [Required] + [StringLength(100)] + public required string Name { get; set; } + + /// + /// Url at which this component is accessible, for use by management tooling + /// + [Required] + [StringLength(200)] + public required string Url { get; set; } + + /// + /// Version number of this ODS component + /// + [Required] + [StringLength(20)] + public required string Version { get; set; } + + public required virtual OdsInstance OdsInstance { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/OwnershipToken.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/OwnershipToken.cs new file mode 100644 index 000000000..92e2c6aeb --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/OwnershipToken.cs @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; + + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + /// + /// Class representing the Ownership tokens with descriptive name. + /// + public class OwnershipToken + { + public OwnershipToken() + { + ApiClients = new Collection(); + } + + /// + /// Ownership token for API clients + /// + [Key] + public short OwnershipTokenId { get; set; } + + /// + /// Descriptive name for the token string length is 50 char + /// + [StringLength(50)] + public string Description { get; set; } = string.Empty; + public virtual ICollection ApiClients { get; set; } + + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/Profile.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/Profile.cs new file mode 100644 index 000000000..383e7d3b0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/Profile.cs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + public class Profile + { + public Profile() + { + Applications = new Collection(); + } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ProfileId { get; set; } + + [Required] + public required string ProfileName { get; set; } + + public virtual ICollection Applications { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/Vendor.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/Vendor.cs new file mode 100644 index 000000000..47b97a3f6 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/Vendor.cs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + public class Vendor + { + public Vendor() + { + Applications = []; + Users = []; + VendorNamespacePrefixes = []; + } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int VendorId { get; set; } + + public string? VendorName { get; set; } + + public virtual ICollection Applications { get; set; } + + public virtual ICollection Users { get; set; } + + public virtual ICollection? VendorNamespacePrefixes { get; set; } + + public Application CreateApplication(string applicationName, string claimSetName) + { + var application = new Application + { + ApplicationName = applicationName, + Vendor = this, + ClaimSetName = claimSetName, + OperationalContextUri = string.Empty + }; + + Applications.Add(application); + return application; + } + + public static Vendor Create(string vendorName, IEnumerable namespacePrefixes) + { + var vendor = new Vendor { VendorName = vendorName }; + + foreach (string namespacePrefix in namespacePrefixes) + { + vendor.VendorNamespacePrefixes ??= []; + + vendor.VendorNamespacePrefixes.Add( + new VendorNamespacePrefix + { + Vendor = vendor, + NamespacePrefix = namespacePrefix + }); + } + + return vendor; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/VendorNamespacePrefix.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/VendorNamespacePrefix.cs new file mode 100644 index 000000000..46a0c9ea6 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/VendorNamespacePrefix.cs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + public class VendorNamespacePrefix + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int VendorNamespacePrefixId { get; set; } + + [Required] + public virtual required Vendor Vendor { get; set; } + + [Required] + [StringLength(255)] + public required string NamespacePrefix { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/WebPagesUsersInRoles.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/WebPagesUsersInRoles.cs new file mode 100644 index 000000000..5f1bd8b0e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Models/WebPagesUsersInRoles.cs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models +{ + [Table("webpages_UsersInRoles")] + public class WebPagesUsersInRoles + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int UserId { get; set; } + + [Required] + public int RoleId { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Providers/AdminDatabaseConnectionStringProvider.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Providers/AdminDatabaseConnectionStringProvider.cs new file mode 100644 index 000000000..7690f50ac --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Providers/AdminDatabaseConnectionStringProvider.cs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common.Configuration; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Providers +{ + public class AdminDatabaseConnectionStringProvider : IAdminDatabaseConnectionStringProvider + { + private readonly IConfigConnectionStringsProvider _configConnectionStringsProvider; + + public AdminDatabaseConnectionStringProvider(IConfigConnectionStringsProvider configConnectionStringsProvider) + { + _configConnectionStringsProvider = configConnectionStringsProvider; + } + + public string GetConnectionString() => _configConnectionStringsProvider.GetConnectionString("EdFi_Admin"); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Providers/IAdminDatabaseConnectionStringProvider.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Providers/IAdminDatabaseConnectionStringProvider.cs new file mode 100644 index 000000000..7a49eeb5e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Providers/IAdminDatabaseConnectionStringProvider.cs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common.Database; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Providers +{ + public interface IAdminDatabaseConnectionStringProvider : IDatabaseConnectionStringProvider { } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Repositories/ClientAppRepo.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Repositories/ClientAppRepo.cs new file mode 100644 index 000000000..2eb35640a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Repositories/ClientAppRepo.cs @@ -0,0 +1,531 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Extensions; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using log4net; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Repositories +{ + public class ClientAppRepo : IClientAppRepo + { + private readonly IUsersContextFactory _contextFactory; + + private readonly ILog _logger = LogManager.GetLogger(typeof(ClientAppRepo)); + private readonly Lazy _defaultOperationalContextUri; + private readonly Lazy _defaultAppName; + private readonly Lazy _defaultClaimSetName; + + public ClientAppRepo( + IUsersContextFactory contextFactory, + IConfigurationRoot config) + { + _contextFactory = Preconditions.ThrowIfNull(contextFactory, nameof(contextFactory)); + Preconditions.ThrowIfNull(config, nameof(config)); + + _defaultOperationalContextUri = new Lazy( + () => config.GetSection("DefaultOperationalContextUri")?.Value ?? string.Empty); + + _defaultAppName = new Lazy( + () => config.GetSection("DefaultApplicationName")?.Value ?? string.Empty); + + _defaultClaimSetName = new Lazy( + () => config.GetSection("DefaultClaimSetName")?.Value ?? string.Empty); + } + + private Profile GetOrCreateProfile(string profileName) + { + using var context = _contextFactory.CreateContext(); + var profiles = context.Profiles.FirstOrDefault(s => s.ProfileName == profileName); + + if (profiles == null) + { + context.Profiles.Add(new Profile { ProfileName = profileName }); + context.SaveChanges(); + } + + return context.Profiles.First(s => s.ProfileName == profileName); + } + + private OwnershipToken GetOrCreateOwnershipToken(string ownershipToken) + { + using var context = _contextFactory.CreateContext(); + var ownershipTokens = context.OwnershipTokens.FirstOrDefault(s => s.Description == ownershipToken); + + if (ownershipTokens == null) + { + context.OwnershipTokens.Add(new OwnershipToken { Description = ownershipToken }); + context.SaveChanges(); + } + + return context.OwnershipTokens.First(s => s.Description == ownershipToken); + } + + public void AddOwnershipTokensToApiClient(string ownershipToken, int apiClientId) + { + using var context = _contextFactory.CreateContext(); + var ownershiptoken = GetOrCreateOwnershipToken(ownershipToken); + + var currentOwnershipToken = context.OwnershipTokens + .Include(u => u.ApiClients) + .FirstOrDefault(u => u.OwnershipTokenId == ownershiptoken.OwnershipTokenId); + + if (currentOwnershipToken != null) + { + var apiClient = context.Clients.FirstOrDefault(a => a.ApiClientId == apiClientId); + if (apiClient != null && !currentOwnershipToken.ApiClients.Any(a => a.ApiClientId == apiClientId)) + { + currentOwnershipToken.ApiClients.Add(apiClient); + } + } + + context.SaveChanges(); + } + + public void AddApiClientOwnershipTokens(List ownershipTokens, int apiClientId) + { + using var context = _contextFactory.CreateContext(); + var apiClientOwnershipTokenList = new List(); + foreach (var ownershipToken in ownershipTokens) + { + var ownershiptoken = context.OwnershipTokens.First(x => x.Description == ownershipToken); + var apiClient = context.Clients.First(u => u.ApiClientId == apiClientId); + apiClientOwnershipTokenList.Add(new ApiClientOwnershipToken + { + ApiClient = apiClient, + OwnershipToken = ownershiptoken + }); + } + context.ApiClientOwnershipTokens.AddRange(apiClientOwnershipTokenList); + context.SaveChanges(); + } + + public void AddProfilesToApplication(List profileNames, int applicationId) + { + using var context = _contextFactory.CreateContext(); + foreach (var profileName in profileNames) + { + var profile = GetOrCreateProfile(profileName); + + var currentProfile = context.Profiles + .Include(u => u.Applications) + .First(u => u.ProfileId == profile.ProfileId); + + if (!currentProfile.Applications.Any(a => a.ApplicationId == applicationId)) + { + var application = context.Applications.First(a => a.ApplicationId == applicationId); + currentProfile.Applications.Add(application); + } + } + + context.SaveChanges(); + } + + public User CreateUser(User user) + { + using (var context = _contextFactory.CreateContext()) + { + context.Users.Add(user); + context.SaveChanges(); + } + + return user; + } + + public IEnumerable GetUsers() + { + using var context = _contextFactory.CreateContext(); + return [.. context.Users.Include(u => u.ApiClients).ThenInclude(ac => ac.Application)]; + } + + public User GetUser(int userId) + { + using var context = _contextFactory.CreateContext(); + return + context.Users.Include(u => u.ApiClients).ThenInclude(ac => ac.Application) + .First(u => u.UserId == userId); + } + + public User GetUser(string userName) + { + using var context = _contextFactory.CreateContext(); + return + context.Users.Include(u => u.ApiClients).ThenInclude(a => a.Application) + .Include(u => u.Vendor) + .First(x => x.Email == userName); + } + + public void DeleteUser(User userProfile) + { + using var context = _contextFactory.CreateContext(); + var user = + context.Users.Include(u => u.ApiClients).ThenInclude(ac => ac.Application) + .FirstOrDefault(x => x.UserId == userProfile.UserId); + + if (user == null) + { + return; + } + + var arraySoThatUnderlyingCollectionCanBeModified = user.ApiClients.ToArray(); + + foreach (var client in arraySoThatUnderlyingCollectionCanBeModified) + { + context.Clients.Remove(client); + } + + context.Users.Remove(user); + context.SaveChanges(); + } + + public ApiClient? GetClient(string key) + { + using var context = _contextFactory.CreateContext(); + return context.Clients + .Include(c => c.Application) + .ThenInclude(c => c!.Vendor) + .ThenInclude(c => c!.VendorNamespacePrefixes) + .Include(c => c.Application) + .ThenInclude(c => c!.Profiles) + .Include(c => c.ApplicationEducationOrganizations) + .Include(c => c.CreatorOwnershipToken) + .FirstOrDefault(c => c.Key == key); + } + + public async Task GetClientAsync(string key) + { + using var context = _contextFactory.CreateContext(); + return await context.Clients + .Include(c => c.Application!) + .ThenInclude(c => c.Vendor) + .ThenInclude(c => c!.VendorNamespacePrefixes) + .Include(c => c.Application!) + .ThenInclude(c => c.Profiles) + .Include(c => c.ApplicationEducationOrganizations) + .Include(c => c.CreatorOwnershipTokenId) + .FirstOrDefaultAsync(c => c.Key == key); + } + + public ApiClient? GetClient(string key, string secret) + { + using var context = _contextFactory.CreateContext(); + return context.Clients.FirstOrDefault(c => c.Key == key && c.Secret == secret); + } + + public ApiClient? GetClientByKey(string key) + { + using var context = _contextFactory.CreateContext(); + return context.Clients.FirstOrDefault(c => c.Key == key); + } + + public ApiClient UpdateClient(ApiClient client) + { + using var context = _contextFactory.CreateContext(); + context.Clients.Update(client); + context.SaveChanges(); + return client; + } + + public void DeleteClient(string key) + { + using var context = _contextFactory.CreateContext(); + var client = context.Clients.First(x => x.Key == key); + + context.ExecuteSqlCommandAsync( + @"delete from dbo.ClientAccessTokens where ApiClient_ApiClientId = @p0; delete from dbo.ApiClients where ApiClientId = @p0", + client.ApiClientId).Wait(); + } + + public Application[] GetVendorApplications(int vendorId) + { + using var context = _contextFactory.CreateContext(); + return [.. context.Applications.Where(a => a.Vendor != null && a.Vendor.VendorId == vendorId)]; + } + + public void AddApiClientToUserWithVendorApplication(int userId, ApiClient client) + { + using var context = _contextFactory.CreateContext(); + var user = context.Users + .Include(u => u.Vendor) + .Include(v => v.Vendor.Applications) + .SingleOrDefault(u => u.UserId == userId); + + if (user == null) + { + return; + } + + if (user.Vendor != null) + { + client.Application = user.Vendor.Applications.FirstOrDefault(); + } + + context.Clients.Add(client); + context.SaveChanges(); + } + + public ApiClient CreateApiClient(int userId, string name, string key, string secret) + { + using var context = _contextFactory.CreateContext(); + var client = CreateApiClient(context, userId, name, SandboxType.Sample, key, secret); + + context.SaveChanges(); + + return client; + } + + public void SetupKeySecret( + string name, + SandboxType sandboxType, + string key, + string secret, + int userId, + int applicationId) + { + using var context = _contextFactory.CreateContext(); + var client = CreateApiClient(context, userId, name, sandboxType, key, secret); + + AddApplicationEducationOrganizations(context, applicationId, client); + + context.SaveChanges(); + } + + private ApiClient CreateApiClient( + IUsersContext context, + int userId, + string name, + SandboxType sandboxType, + string key, + string secret) + { + var attachedUser = context.Users.Find(userId); + + return attachedUser == null + ? throw new InvalidOperationException($"User with ID {userId} not found.") + : attachedUser.AddSandboxClient(name, sandboxType, key, secret); + } + + public void AddEdOrgIdsToApiClient(int userId, int apiClientId, IList edOrgIds, int applicationId) + { + using var context = _contextFactory.CreateContext(); + var application = context.Applications + .Include(a => a.ApplicationEducationOrganizations) + .Single(a => a.ApplicationId == applicationId); + + var user = context.Users.FirstOrDefault(u => u.UserId == userId); + + var client = user?.ApiClients.FirstOrDefault(c => c.ApiClientId == apiClientId); + + if (client == null) + { + return; + } + + client.Application = application; + + foreach (var applicationEducationOrganization in application.ApplicationEducationOrganizations.Where( + s => edOrgIds.Contains(s.EducationOrganizationId))) + { + client.ApplicationEducationOrganizations.Add(applicationEducationOrganization); + } + + context.SaveChanges(); + } + + private void AddApplicationEducationOrganizations(IUsersContext context, int applicationId, ApiClient client) + { + var defaultApplication = context.Applications + .Include(a => a.ApplicationEducationOrganizations) + .First(a => a.ApplicationId == applicationId); + + client.Application = defaultApplication; + + foreach (var applicationEducationOrganization in defaultApplication.ApplicationEducationOrganizations) + { + client.ApplicationEducationOrganizations.Add(applicationEducationOrganization); + } + } + + public ApiClient SetupDefaultSandboxClient( + string name, + SandboxType sandboxType, + string key, + string secret, + int userId, + int applicationId) + { + using var context = _contextFactory.CreateContext(); + _logger.Debug($"Creating API Client"); + var client = GetClient(key, secret) ?? CreateApiClient(context, userId, name, sandboxType, key, secret); + + _logger.Debug($"Adding Education Organization to client"); + AddApplicationEducationOrganizations(context, applicationId, client); + + context.SaveChanges(); + + return client; + } + + public void Reset() + { + try + { + using var context = _contextFactory.CreateContext(); + if (context is DbContext dbContext) + { + try + { + // Admin.Web Creates table webpages_UsersInRoles. + // If exists remove rows, if not swallow exception. + dbContext.DeleteAll(); + context.SaveChanges(); + } + catch (Exception) { } + + dbContext.DeleteAll(); + dbContext.DeleteAll(); + dbContext.DeleteAll(); + dbContext.DeleteAll(); + dbContext.DeleteAll(); + dbContext.DeleteAll(); + dbContext.DeleteAll(); + context.SaveChanges(); + } + } + catch (Exception ex) + { + throw new Exception("Error occurred while attempting to reset Admin database.", ex); + } + } + + public void SetDefaultVendorOnUserFromEmailAndName(string userEmail, string userName) + { + var namespacePrefix = "uri://" + userEmail.Split('@')[1].ToLower(); + + SetDefaultVendorOnUserFromEmailAndName(userEmail, userName, [namespacePrefix]); + } + + public void SetDefaultVendorOnUserFromEmailAndName(string userEmail, string userName, IEnumerable namespacePrefixes) + { + using var context = _contextFactory.CreateContext(); + var vendor = FindOrCreateVendorByDomainName(userName, namespacePrefixes); + + var user = context.Users.FirstOrDefault(u => u.Email.Equals(userEmail)); + + if (user == null) + { + user = User.Create(userEmail, userName, vendor); + } + else + { + user.Vendor = vendor; + } + + context.Vendors.Update(vendor); + context.Users.Update(user); + context.SaveChanges(); + } + + public Vendor CreateOrGetVendor(string userEmail, string userName, IEnumerable namespacePrefixes) + { + var vendorName = userName.Split(',')[0] + .Trim(); + + using var context = _contextFactory.CreateContext(); + var vendor = context.Vendors.SingleOrDefault(v => v.VendorName == vendorName); + + if (vendor == null) + { + vendor = Vendor.Create(vendorName, namespacePrefixes); + context.SaveChanges(); + } + + return vendor; + } + + private Vendor FindOrCreateVendorByDomainName(string vendorName, IEnumerable namespacePrefixes) + { + using var context = _contextFactory.CreateContext(); + var vendor = context.Vendors.FirstOrDefault(v => v.VendorName == vendorName); + + if (vendor != null) + { + return vendor; + } + + var newVendor = Vendor.Create(vendorName, namespacePrefixes); + + context.Vendors.Update(newVendor); + + //TODO: DEA - Move this behavior to happen during client creation. No need to do this in two places. At a minimum, remove the duplicated code. + CreateDefaultApplicationForVendor(newVendor); + + return newVendor!; + } + + public Application CreateApplicationForVendor(int vendorId, string applicationName, string claimSetName) + { + using var context = _contextFactory.CreateContext(); + var app = + context.Applications.SingleOrDefault( + a => a.ApplicationName == applicationName && a.Vendor != null && a.Vendor.VendorId == vendorId); + + if (app != null) + { + return app; + } + + var vendor = context.Vendors.FirstOrDefault(v => v.VendorId == vendorId); + + app = new Application + { + ApplicationName = applicationName, + Vendor = vendor, + ClaimSetName = claimSetName, + OperationalContextUri = _defaultOperationalContextUri.Value + }; + + context.Applications.Update(app); + + context.SaveChanges(); + + return app; + } + + private void CreateDefaultApplicationForVendor(Vendor vendor) + { + using var context = _contextFactory.CreateContext(); + var app = context.Applications.SingleOrDefault( + a => a.ApplicationName == _defaultAppName.Value && a.Vendor != null && a.Vendor.VendorId == vendor.VendorId); + + if (app != null) + { + return; + } + + context.Applications.Update( + new Application + { + ApplicationName = _defaultAppName.Value, + Vendor = vendor, + ClaimSetName = _defaultClaimSetName.Value, + OperationalContextUri = _defaultOperationalContextUri.Value + }); + } + + internal class EmailResult + { + public string Email { get; set; } = string.Empty; + } + + internal class ConfirmationTokenResult + { + public string ConfirmationToken { get; set; } = string.Empty; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Repositories/IClientAppRepo.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Repositories/IClientAppRepo.cs new file mode 100644 index 000000000..6a15b4906 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Repositories/IClientAppRepo.cs @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Repositories +{ + public interface IClientAppRepo + { + + void AddProfilesToApplication(List profileNames, int applicationId); + + IEnumerable GetUsers(); + + User GetUser(int userId); + + User GetUser(string userName); + + User CreateUser(User user); + + void DeleteUser(User userProfile); + + ApiClient? GetClient(string key); + + Task GetClientAsync(string key); + + ApiClient? GetClient(string key, string secret); + + ApiClient UpdateClient(ApiClient client); + + void DeleteClient(string key); + + void SetDefaultVendorOnUserFromEmailAndName(string userEmail, string userName); + + Application[] GetVendorApplications(int vendorId); + + void AddApiClientToUserWithVendorApplication(int userId, ApiClient client); + + ApiClient SetupDefaultSandboxClient(string name, SandboxType sandboxType, string key, string secret, int userId, int applicationId); + + void SetupKeySecret(string name, SandboxType sandboxType, string key, string secret, int userId, int applicationId); + + Vendor CreateOrGetVendor(string userEmail, string userName, IEnumerable namespacePrefixes); + + Application CreateApplicationForVendor(int vendorId, string applicationName, string claimSetName); + + ApiClient CreateApiClient(int userId, string name, string key, string secret); + + void AddEdOrgIdsToApiClient(int userId, int apiClientId, IList edOrgIds, int applicationId); + + void Reset(); + + void SetDefaultVendorOnUserFromEmailAndName(string userEmail, string userName, IEnumerable namespacePrefixes); + + void AddOwnershipTokensToApiClient(string ownershipToken, int apiClientId); + + void AddApiClientOwnershipTokens(List ownershipTokens, int apiClientId); + + ApiClient? GetClientByKey(string key); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/SandboxType.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/SandboxType.cs new file mode 100644 index 000000000..1379bb5ad --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/SandboxType.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess +{ + public enum SandboxType + { + Minimal, + Sample + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Security/EdFiAdminApiClientSecretWriter.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Security/EdFiAdminApiClientSecretWriter.cs new file mode 100644 index 000000000..008c3b597 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Security/EdFiAdminApiClientSecretWriter.cs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common.Security; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Repositories; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Security +{ + public class EdFiAdminApiClientSecretWriter : IApiClientSecretWriter + { + private readonly IApiClientDetailsProvider _apiClientDetailsProvider; + private readonly IClientAppRepo _clientAppRepo; + + public EdFiAdminApiClientSecretWriter( + IApiClientDetailsProvider apiClientDetailsProvider, + IClientAppRepo clientAppRepo) + { + _apiClientDetailsProvider = apiClientDetailsProvider; + _clientAppRepo = clientAppRepo; + } + + public void SetSecret(string key, ApiClientSecret secret) + { + var client = GetClientByKey(key); + + client.Secret = secret.Secret; + client.SecretIsHashed = secret.IsHashed; + _clientAppRepo.UpdateClient(client); + } + + private ApiClient GetClientByKey(string key) + { + var client = _clientAppRepo.GetClientByKey(key); + + if (client == null) + { + throw new ArgumentException($"Invalid key:'{key}'"); + } + + return client; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/DatabaseNameBuilder.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/DatabaseNameBuilder.cs new file mode 100644 index 000000000..13c790f60 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/DatabaseNameBuilder.cs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using EdFi.Common.Configuration; +using EdFi.Common.Database; +using EdFi.Common.Extensions; + +namespace EdFi.Admin.DataAccess.V1.Utils +{ + public class DatabaseNameBuilder : IDatabaseNameBuilder + { + private const string TemplatePrefix = "Ods_"; + private const string SandboxPrefix = TemplatePrefix + "Sandbox_"; + + private const string TemplateEmptyDatabase = TemplatePrefix + "Empty_Template"; + private const string TemplateMinimalDatabase = TemplatePrefix + "Minimal_Template"; + private const string TemplateSampleDatabase = TemplatePrefix + "Populated_Template"; + + private readonly Lazy _databaseNameTemplate; + + public DatabaseNameBuilder(IConfigConnectionStringsProvider connectionStringsProvider, IDbConnectionStringBuilderAdapterFactory connectionStringBuilderFactory) + { + _databaseNameTemplate = new Lazy( + () => + { + if (!connectionStringsProvider.ConnectionStringProviderByName.ContainsKey("EdFi_Ods")) + { + return string.Empty; + } + + var connectionStringBuilder = connectionStringBuilderFactory.Get(); + + connectionStringBuilder.ConnectionString = connectionStringsProvider.GetConnectionString("EdFi_Ods"); + + return connectionStringBuilder.DatabaseName; + }); + } + + public string DemoSandboxDatabase + { + get => "EdFi_Ods"; + } + + public string EmptyDatabase + { + get => DatabaseName(TemplateEmptyDatabase); + } + + public string MinimalDatabase + { + get => DatabaseName(TemplateMinimalDatabase); + } + + public string SampleDatabase + { + get => DatabaseName(TemplateSampleDatabase); + } + + public string SandboxNameForKey(string key) => DatabaseName(SandboxPrefix + key); + + public string KeyFromSandboxName(string sandboxName) => sandboxName.Replace(DatabaseName(SandboxPrefix), string.Empty); + + public string TemplateSandboxNameForKey(string key) => SandboxPrefix + key; + + private string DatabaseName(string databaseName) => _databaseNameTemplate.Value.IsFormatString() + ? string.Format(_databaseNameTemplate.Value, databaseName) + : _databaseNameTemplate.Value; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/DefaultApplicationCreator.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/DefaultApplicationCreator.cs new file mode 100644 index 000000000..afa919afc --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/DefaultApplicationCreator.cs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Admin.DataAccess.V1.Utils +{ + public class DefaultApplicationCreator : IDefaultApplicationCreator + { + private readonly IConfiguration _configuration; + private readonly IUsersContextFactory _usersContextFactory; + + public DefaultApplicationCreator( + IUsersContextFactory usersContextFactory, + IConfiguration configuration) + { + _usersContextFactory = Preconditions.ThrowIfNull(usersContextFactory, nameof(usersContextFactory)); + _configuration = Preconditions.ThrowIfNull(configuration, nameof(configuration)); + } + + /// + /// Look for an existing default application for this particular sandbox type. Also, make sure that all + /// Local Education Agency associations are updated. + /// + /// + /// + /// + public Application FindOrCreateUpdatedDefaultSandboxApplication(int vendorId, SandboxType sandboxType) + { + using (var context = _usersContextFactory.CreateContext()) + { + var vendor = context.Vendors + .Where(x => x.VendorId == vendorId) + .Include(x => x.Applications).ThenInclude(x => x.ApplicationEducationOrganizations) + .Single(); + + var defaultAppName = _configuration.GetSection("DefaultApplicationName").Value ?? "Default Sandbox Application"; + var applicationName = defaultAppName + " " + sandboxType; + var application = GetApplication(context, vendor, applicationName); + + context.SaveChanges(); + return application; + } + } + + public void AddEdOrgIdsToApplication(IList edOrgIds, int applicationId) + { + using (var context = _usersContextFactory.CreateContext()) + { + var application = context.Applications.SingleOrDefault(a => a.ApplicationId == applicationId); + + if (application != null) + { + foreach (var edOrgId in edOrgIds) + { + if (application.ApplicationEducationOrganizations.All(x => x.EducationOrganizationId != edOrgId)) + { + var applicationEducationOrganization = application.CreateApplicationEducationOrganization(edOrgId); + application.ApplicationEducationOrganizations.Add(applicationEducationOrganization); + context.ApplicationEducationOrganizations.Update(applicationEducationOrganization); + } + } + + context.SaveChanges(); + } + } + } + + private Application GetApplication(IUsersContext context, Vendor vendor, string applicationName) + { + if (vendor.Applications.Any(x => x.ApplicationName == applicationName)) + { + return vendor.Applications.First(x => x.ApplicationName == applicationName); + } + + var defaultClaimSetName = _configuration.GetSection("DefaultClaimSetName").Value; + + if (string.IsNullOrWhiteSpace(defaultClaimSetName)) + { + throw new InvalidOperationException("DefaultClaimSetName configuration value is missing or empty."); + } + + var defaultOperationalContextUri = _configuration.GetSection("DefaultOperationalContextUri").Value; + + if (string.IsNullOrWhiteSpace(defaultOperationalContextUri)) + { + throw new InvalidOperationException("DefaultOperationalContextUri configuration value is missing or empty."); + } + + var newApplication = vendor.CreateApplication(applicationName, defaultClaimSetName); + + newApplication.OperationalContextUri = defaultOperationalContextUri; + context.Applications.Add(newApplication); + return newApplication; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/EdFiOdsConnectionStringBuilder.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/EdFiOdsConnectionStringBuilder.cs new file mode 100644 index 000000000..cde905f1c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/EdFiOdsConnectionStringBuilder.cs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Configuration; +using EdFi.Common.Extensions; +using Microsoft.Data.SqlClient; + +namespace EdFi.Ods.AdminApi.V1.Admin.DataAccess.Utils +{ + public class EdFiOdsConnectionStringBuilder + { + /// + /// Builds the connection string to the specified ODS database. + /// + /// The name of the ODS database. + /// The connection string for the specified ODS database. + /// Occurs when the connection strings section doesn't have an entry for "EdFi_Ods". + /// + public static string GetEdFiOdsConnectionString(string databaseName) + { + var connectionStringSettings = System.Configuration.ConfigurationManager.ConnectionStrings["EdFi_Ods"]; + + if (connectionStringSettings == null) + { + throw new ConfigurationErrorsException("The connection string [EdFi_Ods] is required."); + } + + var connectionStringBuilder = + new SqlConnectionStringBuilder(connectionStringSettings.ConnectionString); + + connectionStringBuilder.InitialCatalog = databaseName.IsFormatString() + ? string.Format(connectionStringBuilder.InitialCatalog, databaseName) + : databaseName; + + var connectionString = connectionStringBuilder.ConnectionString; + return connectionString; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/IDatabaseNameBuilder.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/IDatabaseNameBuilder.cs new file mode 100644 index 000000000..d36851f08 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/IDatabaseNameBuilder.cs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Admin.DataAccess.V1.Utils +{ + public interface IDatabaseNameBuilder + { + string EmptyDatabase { get; } + + string MinimalDatabase { get; } + + string SampleDatabase { get; } + + string DemoSandboxDatabase { get; } + + string SandboxNameForKey(string key); + + string KeyFromSandboxName(string sandboxName); + + string TemplateSandboxNameForKey(string sandboxKey); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/IDefaultApplicationCreator.cs b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/IDefaultApplicationCreator.cs new file mode 100644 index 000000000..1208cc433 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Admin.DataAccess/Utils/IDefaultApplicationCreator.cs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Admin.DataAccess; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; + +namespace EdFi.Admin.DataAccess.V1.Utils +{ + public interface IDefaultApplicationCreator + { + Application FindOrCreateUpdatedDefaultSandboxApplication(int vendorId, SandboxType sandboxType); + + void AddEdOrgIdsToApplication(IList edOrgIds, int applicationId); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/EdFi.Ods.AdminApi.V1.csproj b/Application/EdFi.Ods.AdminApi.V1/EdFi.Ods.AdminApi.V1.csproj new file mode 100644 index 000000000..93c83a920 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/EdFi.Ods.AdminApi.V1.csproj @@ -0,0 +1,53 @@ + + + net8.0 + enable + enable + true + NU5100, NU5124 + Linux + ../.. + dev.pgsql.Dockerfile + --no-cache + .env + adminapi-dev + true + Library + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Application/EdFi.Ods.AdminApi/Features/AdminApiError.cs b/Application/EdFi.Ods.AdminApi.V1/Features/AdminApiError.cs similarity index 97% rename from Application/EdFi.Ods.AdminApi/Features/AdminApiError.cs rename to Application/EdFi.Ods.AdminApi.V1/Features/AdminApiError.cs index 9148f116c..ce14188e9 100644 --- a/Application/EdFi.Ods.AdminApi/Features/AdminApiError.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Features/AdminApiError.cs @@ -6,7 +6,7 @@ using FluentValidation.Results; using Swashbuckle.AspNetCore.Annotations; -namespace EdFi.Ods.AdminApi.Features; +namespace EdFi.Ods.AdminApi.V1.Features; [SwaggerSchema(Title = "AdminApiError", Description = "Wrapper schema for all error responses")] public class AdminApiError diff --git a/Application/EdFi.Ods.AdminApi/Features/AdminApiResponse.cs b/Application/EdFi.Ods.AdminApi.V1/Features/AdminApiResponse.cs similarity index 97% rename from Application/EdFi.Ods.AdminApi/Features/AdminApiResponse.cs rename to Application/EdFi.Ods.AdminApi.V1/Features/AdminApiResponse.cs index 5720e34fa..604866a1e 100644 --- a/Application/EdFi.Ods.AdminApi/Features/AdminApiResponse.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Features/AdminApiResponse.cs @@ -2,14 +2,14 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. - -using Newtonsoft.Json; + +using Newtonsoft.Json; using Swashbuckle.AspNetCore.Annotations; -using System.Net; -using System.Net.Mime; -using System.Text; - -namespace EdFi.Ods.AdminApi.Features; +using System.Net; +using System.Net.Mime; +using System.Text; + +namespace EdFi.Ods.AdminApi.V1.Features; public class AdminApiResponse { @@ -34,83 +34,83 @@ public class AdminApiResponse : AdminApiResponse protected AdminApiResponse(int status, T result) : base(status) { Result = result; } protected AdminApiResponse(int status, string title, T result) : base(status, title) { Result = result; } - public static IResult Ok(T result, JsonSerializerSettings? jsonSerializerSettings = null) - { - var statusCode = HttpStatusCode.OK; - var message = "Request successful"; - if (jsonSerializerSettings != null) - { - var jsonResponse = SerializeObjectAsJson(result, statusCode, message, jsonSerializerSettings); - return new JsonSerializeResult(jsonResponse, statusCode); - } - else - { - return Results.Ok(new AdminApiResponse((int)statusCode, message, result)); - } + public static IResult Ok(T result, JsonSerializerSettings? jsonSerializerSettings = null) + { + var statusCode = HttpStatusCode.OK; + var message = "Request successful"; + if (jsonSerializerSettings != null) + { + var jsonResponse = SerializeObjectAsJson(result, statusCode, message, jsonSerializerSettings); + return new JsonSerializeResult(jsonResponse, statusCode); + } + else + { + return Results.Ok(new AdminApiResponse((int)statusCode, message, result)); + } + } + + public static IResult Created(T result, string name, string getUri, JsonSerializerSettings? jsonSerializerSettings = null) + { + var statusCode = HttpStatusCode.Created; + var message = $"{name} created successfully"; + if (jsonSerializerSettings != null) + { + var jsonResponse = SerializeObjectAsJson(result, statusCode, message, jsonSerializerSettings); + return new JsonSerializeResult(jsonResponse, statusCode, getUri); + } + else + { + return Results.Created(getUri, new AdminApiResponse((int)statusCode, message, result)); ; + } } - public static IResult Created(T result, string name, string getUri, JsonSerializerSettings? jsonSerializerSettings = null) - { - var statusCode = HttpStatusCode.Created; - var message = $"{name} created successfully"; - if (jsonSerializerSettings != null) - { - var jsonResponse = SerializeObjectAsJson(result, statusCode, message, jsonSerializerSettings); - return new JsonSerializeResult(jsonResponse, statusCode, getUri); - } - else - { - return Results.Created(getUri, new AdminApiResponse((int)statusCode, message, result)); ; - } - } - - public static IResult Updated(T result, string name, JsonSerializerSettings? jsonSerializerSettings = null) - { - var statusCode = HttpStatusCode.OK; - var message = $"{name} updated successfully"; - - if (jsonSerializerSettings != null) - { - var jsonResponse = SerializeObjectAsJson(result, statusCode, message, jsonSerializerSettings); - return new JsonSerializeResult(jsonResponse, statusCode); - } - else - { - return Results.Ok(new AdminApiResponse((int)statusCode, message, result)); - } - } - - private static string SerializeObjectAsJson(T result, HttpStatusCode statusCode, string message, JsonSerializerSettings jsonSerializerSettings) - { - var data = new AdminApiResponse((int)statusCode, message, result); - var dataSerialize = JsonConvert.SerializeObject(data, Formatting.Indented, jsonSerializerSettings); - - return dataSerialize; + public static IResult Updated(T result, string name, JsonSerializerSettings? jsonSerializerSettings = null) + { + var statusCode = HttpStatusCode.OK; + var message = $"{name} updated successfully"; + + if (jsonSerializerSettings != null) + { + var jsonResponse = SerializeObjectAsJson(result, statusCode, message, jsonSerializerSettings); + return new JsonSerializeResult(jsonResponse, statusCode); + } + else + { + return Results.Ok(new AdminApiResponse((int)statusCode, message, result)); + } + } + + private static string SerializeObjectAsJson(T result, HttpStatusCode statusCode, string message, JsonSerializerSettings jsonSerializerSettings) + { + var data = new AdminApiResponse((int)statusCode, message, result); + var dataSerialize = JsonConvert.SerializeObject(data, Formatting.Indented, jsonSerializerSettings); + + return dataSerialize; } } - - -public class JsonSerializeResult : IResult -{ - private readonly string _jsonResponse; - private readonly HttpStatusCode _statusCode; - private readonly string _location; - + + +public class JsonSerializeResult : IResult +{ + private readonly string _jsonResponse; + private readonly HttpStatusCode _statusCode; + private readonly string _location; + public JsonSerializeResult(string jsonResponse, HttpStatusCode statusCode, string location = "") { - _jsonResponse = jsonResponse; - _statusCode = statusCode; - _location = location; - } - public async Task ExecuteAsync(HttpContext httpContext) - { - httpContext.Response.ContentType = MediaTypeNames.Application.Json; - httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_jsonResponse); - httpContext.Response.StatusCode = (int)_statusCode; - - if (!string.IsNullOrEmpty(_location)) - httpContext.Response.Headers.Location = _location; - - await httpContext.Response.WriteAsync(_jsonResponse); - } -} + _jsonResponse = jsonResponse; + _statusCode = statusCode; + _location = location; + } + public async Task ExecuteAsync(HttpContext httpContext) + { + httpContext.Response.ContentType = MediaTypeNames.Application.Json; + httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_jsonResponse); + httpContext.Response.StatusCode = (int)_statusCode; + + if (!string.IsNullOrEmpty(_location)) + httpContext.Response.Headers.Location = _location; + + await httpContext.Response.WriteAsync(_jsonResponse); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Applications/AddApplication.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/AddApplication.cs new file mode 100644 index 000000000..4de0d3474 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/AddApplication.cs @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Infrastructure.Commands; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.V1.Infrastructure.Documentation; +using FluentValidation; +using FluentValidation.Results; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.Applications; + +public class AddApplication : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPost(endpoints, "/applications", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(201)) + .BuildForVersions(AdminApiVersions.V1); + } + + public async Task Handle(Validator validator, IAddApplicationCommand addApplicationCommand, IMapper mapper, IUsersContext db, AddApplicationRequest request) + { + await validator.GuardAsync(request); + GuardAgainstInvalidEntityReferences(request, db); + var addedApplicationResult = addApplicationCommand.Execute(request); + var model = mapper.Map(addedApplicationResult); + return AdminApiResponse.Created(model, "Application", $"/applications/{model.ApplicationId}"); + } + + private void GuardAgainstInvalidEntityReferences(AddApplicationRequest request, IUsersContext db) + { + if (null == db.Vendors.Find(request.VendorId)) + throw new ValidationException(new[] { new ValidationFailure(nameof(request.VendorId), $"Vendor with ID {request.VendorId} not found.") }); + + if (request.ProfileId.HasValue && db.Profiles.Find(request.ProfileId) == null) + throw new ValidationException(new[] { new ValidationFailure(nameof(request.ProfileId), $"Profile with ID {request.ProfileId} not found.") }); + + if (request.OdsInstanceId.HasValue && db.OdsInstances.Find(request.OdsInstanceId) == null) + throw new ValidationException(new[] { new ValidationFailure(nameof(request.OdsInstanceId), $"Ods instance with ID {request.OdsInstanceId} not found.") }); + } + + [SwaggerSchema(Title = "AddApplicationRequest")] + public class AddApplicationRequest : IAddApplicationModel + { + [SwaggerSchema(Description = FeatureConstants.ApplicationNameDescription, Nullable = false)] + public string ApplicationName { get; set; } = string.Empty; + + [SwaggerSchema(Description = FeatureConstants.VendorIdDescription, Nullable = false)] + public int VendorId { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] + public string? ClaimSetName { get; set; } + + [SwaggerOptional] + [SwaggerSchema(Description = FeatureConstants.ProfileIdDescription)] + public int? ProfileId { get; set; } + + [SwaggerOptional] + [SwaggerSchema(Description = FeatureConstants.OdsInstanceIdDescription)] + public int? OdsInstanceId { get; set; } + + [SwaggerSchema(Description = FeatureConstants.EducationOrganizationIdsDescription, Nullable = false)] + public IEnumerable? EducationOrganizationIds { get; set; } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(m => m.ApplicationName) + .NotEmpty(); + + RuleFor(m => m.ApplicationName) + .Must(BeWithinApplicationNameMaxLength) + .WithMessage(FeatureConstants.ApplicationNameLengthValidationMessage) + .When(x => x.ApplicationName != null); + + RuleFor(m => m.ClaimSetName) + .NotEmpty() + .WithMessage(FeatureConstants.ClaimSetNameValidationMessage); + + RuleFor(m => m.EducationOrganizationIds) + .NotEmpty() + .WithMessage(FeatureConstants.EdOrgIdsValidationMessage); + + RuleFor(m => m.VendorId).Must(id => id > 0).WithMessage(FeatureConstants.VendorIdValidationMessage); + } + + private bool BeWithinApplicationNameMaxLength(IAddApplicationModel model, string? applicationName, ValidationContext context) + { + var extraCharactersInName = applicationName!.Length - ValidationConstants.MaximumApplicationNameLength; + if (extraCharactersInName <= 0) + { + return true; + } + context.MessageFormatter.AppendArgument("ApplicationName", applicationName); + context.MessageFormatter.AppendArgument("ExtraCharactersInName", extraCharactersInName); + return false; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Applications/ApplicationModel.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/ApplicationModel.cs new file mode 100644 index 000000000..326fad15d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/ApplicationModel.cs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.Applications; + +[SwaggerSchema(Title = "Application")] +public class ApplicationModel +{ + public int ApplicationId { get; set; } + public string? ApplicationName { get; set; } + public string? ClaimSetName { get; set; } + public string? ProfileName { get; set; } + public IList? EducationOrganizationIds { get; set; } + public string? OdsInstanceName { get; set; } + public int? OdsInstanceId { get; set; } + public int? VendorId { get; set; } + public IList? Profiles { get; set; } +} + +[SwaggerSchema(Title = "ApplicationKeySecret")] +public class ApplicationResult +{ + public int ApplicationId { get; set; } + public string? Key { get; set; } + public string? Secret { get; set; } +} + +[SwaggerSchema(Title = "Profile")] +public class Profile +{ + public int? Id { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Applications/DeleteApplication.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/DeleteApplication.cs new file mode 100644 index 000000000..c6470f8cd --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/DeleteApplication.cs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; + +namespace EdFi.Ods.AdminApi.V1.Features.Applications; + +public class DeleteApplication : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapDelete(endpoints, "/applications/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V1); + } + + public Task Handle(IDeleteApplicationCommand deleteApplicationCommand, int id) + { + deleteApplicationCommand.Execute(id); + return Task.FromResult(AdminApiResponse.Deleted("Application")); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Applications/EditApplication.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/EditApplication.cs new file mode 100644 index 000000000..0b9b35d74 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/EditApplication.cs @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Infrastructure.Commands; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.V1.Infrastructure.Documentation; +using FluentValidation; +using FluentValidation.Results; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.Applications; + +public class EditApplication : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPut(endpoints, "/applications/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V1); + } + + public async Task Handle(IEditApplicationCommand editApplicationCommand, IMapper mapper, + Validator validator, IUsersContext db, EditApplicationRequest request, int id) + { + request.ApplicationId = id; + await validator.GuardAsync(request); + GuardAgainstInvalidEntityReferences(request, db); + + var updatedApplication = editApplicationCommand.Execute(request); + var model = mapper.Map(updatedApplication); + return AdminApiResponse.Updated(model, "Application"); + } + + private static void GuardAgainstInvalidEntityReferences(EditApplicationRequest request, IUsersContext db) + { + if (null == db.Vendors.Find(request.VendorId)) + throw new ValidationException([new ValidationFailure(nameof(request.VendorId), $"Vendor with ID {request.VendorId} not found.")]); + + if (request.ProfileId.HasValue && db.Profiles.Find(request.ProfileId) == null) + throw new ValidationException([new ValidationFailure(nameof(request.ProfileId), $"Profile with ID {request.ProfileId} not found.")]); + } + + [SwaggerSchema(Title = "EditApplicationRequest")] + public class EditApplicationRequest : IEditApplicationModel + { + [SwaggerSchema(Description = "Application id", Nullable = false)] + public int ApplicationId { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ApplicationNameDescription, Nullable = false)] + public string ApplicationName { get; set; } = string.Empty; + + [SwaggerSchema(Description = FeatureConstants.VendorIdDescription, Nullable = false)] + public int VendorId { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] + public string? ClaimSetName { get; set; } + + [SwaggerOptional] + [SwaggerSchema(Description = FeatureConstants.ProfileIdDescription)] + public int? ProfileId { get; set; } + + [SwaggerOptional] + [SwaggerSchema(Description = FeatureConstants.OdsInstanceIdDescription)] + public int? OdsInstanceId { get; set; } + + [SwaggerSchema(Description = FeatureConstants.EducationOrganizationIdsDescription, Nullable = false)] + public IEnumerable? EducationOrganizationIds { get; set; } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(m => m.ApplicationId).NotEmpty(); + RuleFor(m => m.ApplicationName).NotEmpty(); + RuleFor(m => m.ApplicationName) + .Must(BeWithinApplicationNameMaxLength) + .WithMessage(FeatureConstants.ApplicationNameLengthValidationMessage) + .When(x => x.ApplicationName != null); + RuleFor(m => m.ClaimSetName) + .NotEmpty() + .WithMessage(FeatureConstants.ClaimSetNameValidationMessage); + + RuleFor(m => m.EducationOrganizationIds) + .NotEmpty() + .WithMessage(FeatureConstants.EdOrgIdsValidationMessage); + + RuleFor(m => m.VendorId).Must(id => id > 0).WithMessage(FeatureConstants.VendorIdValidationMessage); + + static bool BeWithinApplicationNameMaxLength(IEditApplicationModel model, string? applicationName, ValidationContext context) + { + var extraCharactersInName = applicationName!.Length - ValidationConstants.MaximumApplicationNameLength; + if (extraCharactersInName <= 0) + { + return true; + } + context.MessageFormatter.AppendArgument("ApplicationName", applicationName); + context.MessageFormatter.AppendArgument("ExtraCharactersInName", extraCharactersInName); + return false; + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Applications/ReadApplication.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/ReadApplication.cs new file mode 100644 index 000000000..818d2c4b6 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/ReadApplication.cs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +namespace EdFi.Ods.AdminApi.V1.Features.Applications; + +public class ReadApplication : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/applications", GetApplications) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V1); + + AdminApiEndpointBuilder.MapGet(endpoints, "/applications/{id}", GetApplication) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V1); + } + + internal Task GetApplications( + IGetApplicationsQuery getApplicationsAndApplicationsQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams) + { + var applications = getApplicationsAndApplicationsQuery.Execute(commonQueryParams); + return Task.FromResult(AdminApiResponse>.Ok(mapper.Map>(applications))); + } + + internal Task GetApplication(GetApplicationByIdQuery getApplicationByIdQuery, IMapper mapper, int id) + { + var application = getApplicationByIdQuery.Execute(id); + if (application == null) + { + throw new NotFoundException("application", id); + } + var model = mapper.Map(application); + return Task.FromResult(AdminApiResponse.Ok(model)); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Applications/ReadApplicationsByVendor.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/ReadApplicationsByVendor.cs new file mode 100644 index 000000000..d25593482 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/ReadApplicationsByVendor.cs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +namespace EdFi.Ods.AdminApi.V1.Features.Applications; + +public class ReadApplicationsByVendor : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + var url = "vendors/{id}/applications"; + + AdminApiEndpointBuilder.MapGet(endpoints, url, GetVendorApplications) + .WithSummaryAndDescription("Retrieves applications assigned to a specific vendor based on the resource identifier.", "Retrieves applications assigned to a specific vendor based on the resource identifier.") + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V1); + } + + internal Task GetVendorApplications(GetApplicationsByVendorIdQuery getApplicationByVendorIdQuery, IMapper mapper, int id) + { + var vendorApplications = mapper.Map>(getApplicationByVendorIdQuery.Execute(id)); + return Task.FromResult(AdminApiResponse>.Ok(vendorApplications)); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Applications/ResetApplicationCredentials.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/ResetApplicationCredentials.cs new file mode 100644 index 000000000..8116acb3f --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Applications/ResetApplicationCredentials.cs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; + +namespace EdFi.Ods.AdminApi.V1.Features.Applications; + +public class ResetApplicationCredentials : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPut(endpoints, "/applications/{id}/reset-credential", HandleResetCredentials) + .WithSummaryAndDescription("Reset application credentials. Returns new key and secret.", "Reset application credentials. Returns new key and secret.") + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V1); + } + + public async Task HandleResetCredentials(RegenerateApiClientSecretCommand resetSecretCommand, IMapper mapper, int id) + { + var resetApplicationSecret = await Task.Run(() => resetSecretCommand.Execute(id)); + var model = mapper.Map(resetApplicationSecret); + return AdminApiResponse.Updated(model, "Application secret"); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/AddClaimSet.cs b/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/AddClaimSet.cs new file mode 100644 index 000000000..46802a644 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/AddClaimSet.cs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.V1.Infrastructure.JsonContractResolvers; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using FluentValidation; +using Newtonsoft.Json; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.ClaimSets; + +public class AddClaimSet : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPost(endpoints, "/claimsets", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(201)) + .BuildForVersions(AdminApiVersions.V1); + } + + public async Task Handle(Validator validator, AddClaimSetCommand addClaimSetCommand, + AddOrEditResourcesOnClaimSetCommand addOrEditResourcesOnClaimSetCommand, + IGetClaimSetByIdQuery getClaimSetByIdQuery, + IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, + IGetApplicationsByClaimSetIdQuery getApplications, + IAuthStrategyResolver strategyResolver, + IMapper mapper, + Request request) + { + await validator.GuardAsync(request); + var addedClaimSetId = addClaimSetCommand.Execute(new AddClaimSetModel + { + ClaimSetName = request.Name ?? string.Empty + }); + + var resourceClaims = mapper.Map>(request.ResourceClaims); + + var resolvedResourceClaims = strategyResolver.ResolveAuthStrategies(resourceClaims).ToList(); + + addOrEditResourcesOnClaimSetCommand.Execute(addedClaimSetId, resolvedResourceClaims); + + var claimSet = getClaimSetByIdQuery.Execute(addedClaimSetId); + + var model = mapper.Map(claimSet); + model.ApplicationsCount = getApplications.ExecuteCount(addedClaimSetId); + model.ResourceClaims = getResourcesByClaimSetIdQuery.AllResources(addedClaimSetId) + .Select(r => mapper.Map(r)).ToList(); + + + return AdminApiResponse.Created( + model, + "ClaimSet", + $"/claimsets/{addedClaimSetId}", + new JsonSerializerSettings() + { + Formatting = Formatting.Indented, + ContractResolver = new ShouldSerializeContractResolver() + }); + } + + [SwaggerSchema(Title = "AddClaimSetRequest")] + public class Request + { + [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] + public string? Name { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ResourceClaimsDescription, Nullable = false)] + public List? ResourceClaims { get; set; } + } + + public class Validator : AbstractValidator + { + private readonly IGetAllClaimSetsQuery _getAllClaimSetsQuery; + + public Validator(IGetAllClaimSetsQuery getAllClaimSetsQuery, + IGetResourceClaimsAsFlatListQuery getResourceClaimsAsFlatListQuery, + IGetAllAuthorizationStrategiesQuery getAllAuthorizationStrategiesQuery, + IMapper mapper) + { + _getAllClaimSetsQuery = getAllClaimSetsQuery; + + var resourceClaims = (Lookup)getResourceClaimsAsFlatListQuery.Execute() + .ToLookup(rc => rc.Name?.ToLower()); + + var authStrategyNames = getAllAuthorizationStrategiesQuery.Execute() + .Select(a => a.AuthStrategyName).ToList(); + + RuleFor(m => m.Name).NotEmpty() + .Must(BeAUniqueName) + .WithMessage(FeatureConstants.ClaimSetAlreadyExistsMessage); + + RuleFor(m => m.Name) + .MaximumLength(255) + .WithMessage(FeatureConstants.ClaimSetNameMaxLengthMessage); + + RuleFor(m => m).Custom((claimSet, context) => + { + var resourceClaimValidator = new ResourceClaimValidator(); + + if (claimSet.ResourceClaims != null && claimSet.ResourceClaims.Count != 0) + { + foreach (var resourceClaim in claimSet.ResourceClaims) + { + resourceClaimValidator.Validate(resourceClaims, authStrategyNames, + resourceClaim, mapper.Map>(claimSet.ResourceClaims), context, claimSet.Name); + } + } + }); + } + + private bool BeAUniqueName(string? name) + { + return _getAllClaimSetsQuery.Execute().All(x => x.Name != name); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/ClaimSetModel.cs b/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/ClaimSetModel.cs new file mode 100644 index 000000000..24073e0d6 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/ClaimSetModel.cs @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.ClaimSets; + +[SwaggerSchema(Title = "ClaimSet")] +public class ClaimSetModel +{ + public int Id { get; set; } + public string? Name { get; set; } + public bool IsSystemReserved { get; set; } + public int ApplicationsCount { get; set; } +} + +[SwaggerSchema(Title = "ClaimSetWithResources")] +public class ClaimSetDetailsModel : ClaimSetModel +{ + public List ResourceClaims { get; set; } = new(); +} + +[SwaggerSchema(Title = "ResourceClaim")] +public class ResourceClaimModel +{ + public string? Name { get; set; } + public bool Create { get; set; } + public bool Read { get; set; } + public bool Update { get; set; } + public bool Delete { get; set; } + public bool ReadChanges { get; set; } + public AuthorizationStrategiesModel?[] DefaultAuthStrategiesForCRUD { get; set; } + public AuthorizationStrategiesModel?[] AuthStrategyOverridesForCRUD { get; set; } + + [SwaggerSchema(Description = "Children are collection of ResourceClaim")] + public List Children { get; set; } + public ResourceClaimModel() + { + Children = new List(); + DefaultAuthStrategiesForCRUD = Array.Empty(); + AuthStrategyOverridesForCRUD = Array.Empty(); + } +} + +[SwaggerSchema(Title = "ResourceClaim")] +public class RequestResourceClaimModel +{ + public string? Name { get; set; } + public bool Create { get; set; } + public bool Read { get; set; } + public bool Update { get; set; } + public bool Delete { get; set; } + public bool ReadChanges { get; set; } + public AuthorizationStrategiesModel?[] AuthStrategyOverridesForCRUD { get; set; } + + [SwaggerSchema(Description = "Children are collection of ResourceClaim")] + public List Children { get; set; } + public RequestResourceClaimModel() + { + Children = new List(); + AuthStrategyOverridesForCRUD = Array.Empty(); + } +} + +public class ChildrenRequestResourceClaimModel : RequestResourceClaimModel +{ + public new List Children { get; set; } = new List(); +} + +[SwaggerSchema(Title = "AuthorizationStrategies")] +public class AuthorizationStrategiesModel +{ + public AuthorizationStrategyModel?[] AuthorizationStrategies { get; set; } + public AuthorizationStrategiesModel() + { + AuthorizationStrategies = Array.Empty(); + + } +} + +[SwaggerSchema(Title = "AuthorizationStrategy")] +public class AuthorizationStrategyModel +{ + //[SwaggerExclude] + public int AuthStrategyId { get; set; } + + public string? AuthStrategyName { get; set; } + + //[SwaggerExclude] + public string? DisplayName { get; set; } + + public bool IsInheritedFromParent { get; set; } +} + +public class EditClaimSetModel : IEditClaimSetModel +{ + public string? ClaimSetName { get; set; } + + public int ClaimSetId { get; set; } +} + +public class UpdateResourcesOnClaimSetModel : IUpdateResourcesOnClaimSetModel +{ + public int ClaimSetId { get; set; } + + public List? ResourceClaims { get; set; } = new List(); +} + +public class DeleteClaimSetModel : IDeleteClaimSetModel +{ + public string? Name { get; set; } + + public int Id { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/DeleteClaimSet.cs b/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/DeleteClaimSet.cs new file mode 100644 index 000000000..662a8e9b2 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/DeleteClaimSet.cs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; + +namespace EdFi.Ods.AdminApi.V1.Features.ClaimSets; + +public class DeleteClaimSet : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapDelete(endpoints, "/claimsets/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V1); + } + + public Task Handle(IDeleteClaimSetCommand deleteClaimSetCommand, [FromServices] IGetClaimSetByIdQuery getClaimSetByIdQuery, IGetApplicationsByClaimSetIdQuery getApplications, int id) + { + CheckClaimSetExists(id, getClaimSetByIdQuery); + CheckAgainstDeletingClaimSetsWithApplications(id, getApplications); + + try + { + deleteClaimSetCommand.Execute(new DeleteClaimSetModel { Id = id }); + } + catch (AdminApiException exception) + { + throw new ValidationException([new ValidationFailure(nameof(id), exception.Message)]); + } + + return Task.FromResult(AdminApiResponse.Deleted("ClaimSet")); + } + + private static void CheckClaimSetExists(int id, IGetClaimSetByIdQuery query) + { + try + { + query.Execute(id); + } + catch (AdminApiException) + { + throw new NotFoundException("claimset", id); + } + } + + private static void CheckAgainstDeletingClaimSetsWithApplications(int id, IGetApplicationsByClaimSetIdQuery getApplications) + { + var associatedApplicationsCount = getApplications.Execute(id).Count(); + if (associatedApplicationsCount > 0) + throw new ValidationException([ new ValidationFailure(nameof(id), + $"Cannot delete this claim set. This claim set has {associatedApplicationsCount} associated application(s).") ]); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/EditClaimSet.cs b/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/EditClaimSet.cs new file mode 100644 index 000000000..c539ccb45 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/EditClaimSet.cs @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.V1.Infrastructure.JsonContractResolvers; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using FluentValidation; +using FluentValidation.Results; +using Newtonsoft.Json; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.ClaimSets; + +public class EditClaimSet : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPut(endpoints, "/claimsets/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V1); + } + + public async Task Handle(Validator validator, IEditClaimSetCommand editClaimSetCommand, + UpdateResourcesOnClaimSetCommand updateResourcesOnClaimSetCommand, + IGetClaimSetByIdQuery getClaimSetByIdQuery, + IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, + IGetApplicationsByClaimSetIdQuery getApplications, + IAuthStrategyResolver strategyResolver, + IMapper mapper, + EditClaimSetRequest request, int id) + { + request.Id = id; + await validator.GuardAsync(request); + + var editClaimSetModel = new EditClaimSetModel + { + ClaimSetName = request.Name, + ClaimSetId = id + }; + + int updatedClaimSetId; + try + { + updatedClaimSetId = editClaimSetCommand.Execute(editClaimSetModel); + } + catch (AdminApiException exception) + { + throw new ValidationException([new ValidationFailure(nameof(id), exception.Message)]); + } + + var resourceClaims = mapper.Map>(request.ResourceClaims); + var resolvedResourceClaims = strategyResolver.ResolveAuthStrategies(resourceClaims).ToList(); + + updateResourcesOnClaimSetCommand.Execute( + new UpdateResourcesOnClaimSetModel { ClaimSetId = updatedClaimSetId, ResourceClaims = resolvedResourceClaims }); + + var claimSet = getClaimSetByIdQuery.Execute(updatedClaimSetId); + + var model = mapper.Map(claimSet); + model.ApplicationsCount = getApplications.ExecuteCount(updatedClaimSetId); + model.ResourceClaims = getResourcesByClaimSetIdQuery.AllResources(updatedClaimSetId) + .Select(r => mapper.Map(r)).ToList(); + + return AdminApiResponse.Updated( + model, + "ClaimSet", + new JsonSerializerSettings() + { + Formatting = Formatting.Indented, + ContractResolver = new ShouldSerializeContractResolver() + }); + } + + [SwaggerSchema(Title = "EditClaimSetRequest")] + public class EditClaimSetRequest + { + [SwaggerSchema(Description = "ClaimSet id", Nullable = false)] + public int Id { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] + public string? Name { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ResourceClaimsDescription, Nullable = false)] + public List? ResourceClaims { get; set; } + } + + public class Validator : AbstractValidator + { + private readonly IGetClaimSetByIdQuery _getClaimSetByIdQuery; + private readonly IGetAllClaimSetsQuery _getAllClaimSetsQuery; + + public Validator(IGetClaimSetByIdQuery getClaimSetByIdQuery, + IGetAllClaimSetsQuery getAllClaimSetsQuery, + IGetResourceClaimsAsFlatListQuery getResourceClaimsAsFlatListQuery, + IGetAllAuthorizationStrategiesQuery getAllAuthorizationStrategiesQuery, + IMapper mapper) + { + _getClaimSetByIdQuery = getClaimSetByIdQuery; + _getAllClaimSetsQuery = getAllClaimSetsQuery; + + var resourceClaims = (Lookup)getResourceClaimsAsFlatListQuery.Execute() + .ToLookup(rc => rc.Name?.ToLower()); + + var authStrategyNames = getAllAuthorizationStrategiesQuery.Execute() + .Select(a => a.AuthStrategyName).ToList(); + + RuleFor(m => m.Id).NotEmpty(); + + RuleFor(m => m.Id) + .Must(BeAnExistingClaimSet) + .WithMessage(FeatureConstants.ClaimSetNotFound); + + RuleFor(m => m.Name) + .NotEmpty() + .Must(BeAUniqueName) + .WithMessage(FeatureConstants.ClaimSetAlreadyExistsMessage) + .When(m => BeAnExistingClaimSet(m.Id) && NameIsChanged(m)); + + RuleFor(m => m.Name) + .MaximumLength(255) + .WithMessage(FeatureConstants.ClaimSetNameMaxLengthMessage); + + RuleFor(m => m).Custom((claimSet, context) => + { + var resourceClaimValidator = new ResourceClaimValidator(); + + if (claimSet.ResourceClaims != null && claimSet.ResourceClaims.Any()) + { + foreach (var resourceClaim in claimSet.ResourceClaims) + { + resourceClaimValidator.Validate(resourceClaims, authStrategyNames, + resourceClaim, mapper.Map>(claimSet.ResourceClaims), context, claimSet.Name); + } + } + }); + } + + private bool BeAnExistingClaimSet(int id) + { + try + { + _getClaimSetByIdQuery.Execute(id); + return true; + } + catch (AdminApiException) + { + throw new NotFoundException("claimSet", id); + } + } + + private bool NameIsChanged(EditClaimSetRequest model) + { + return _getClaimSetByIdQuery.Execute(model.Id).Name != model.Name; + } + + private bool BeAUniqueName(string? name) + { + return _getAllClaimSetsQuery.Execute().All(x => x.Name != name); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/ReadClaimSets.cs b/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/ReadClaimSets.cs new file mode 100644 index 000000000..14168fe11 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/ReadClaimSets.cs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Infrastructure.JsonContractResolvers; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using Newtonsoft.Json; + +namespace EdFi.Ods.AdminApi.V1.Features.ClaimSets; + +public class ReadClaimSets : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/claimsets", GetClaimSets) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse>(200)) + .BuildForVersions(AdminApiVersions.V1); + + AdminApiEndpointBuilder.MapGet(endpoints, "/claimsets/{id}", GetClaimSet) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V1); + } + + internal Task GetClaimSets( + IGetAllClaimSetsQuery getClaimSetsQuery, IGetApplicationsByClaimSetIdQuery getApplications, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams) + { + var claimSets = getClaimSetsQuery.Execute(commonQueryParams).ToList(); + var model = mapper.Map>(claimSets); + foreach (var claimSet in model) + { + claimSet.ApplicationsCount = getApplications.ExecuteCount(claimSet.Id); + } + return Task.FromResult(AdminApiResponse>.Ok(model)); + } + + internal Task GetClaimSet(IGetClaimSetByIdQuery getClaimSetByIdQuery, + IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, + IGetApplicationsByClaimSetIdQuery getApplications, + IMapper mapper, int id) + { + ClaimSet claimSet; + try + { + claimSet = getClaimSetByIdQuery.Execute(id); + } + catch (AdminApiException) + { + throw new NotFoundException("claimset", id); + } + + var allResources = getResourcesByClaimSetIdQuery.AllResources(id); + var claimSetData = mapper.Map(claimSet); + claimSetData.ApplicationsCount = getApplications.ExecuteCount(id); + claimSetData.ResourceClaims = mapper.Map>(allResources.ToList()); + return Task.FromResult(AdminApiResponse.Ok(claimSetData, + new JsonSerializerSettings() + { + Formatting = Formatting.Indented, + ContractResolver = new ShouldSerializeContractResolver() + })); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/ResourceClaimValidator.cs b/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/ResourceClaimValidator.cs new file mode 100644 index 000000000..1ad697417 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/ClaimSets/ResourceClaimValidator.cs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Data; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using FluentValidation; + +namespace EdFi.Ods.AdminApi.V1.Features.ClaimSets; + +public class ResourceClaimValidator +{ + private static List? _duplicateResources; + + public ResourceClaimValidator() + { + _duplicateResources = []; + } + + public void Validate(Lookup dbResourceClaims, + List dbAuthStrategies, RequestResourceClaimModel resourceClaim, List existingResourceClaims, + ValidationContext context, string? claimSetName) + { + context.MessageFormatter.AppendArgument("ClaimSetName", claimSetName); + context.MessageFormatter.AppendArgument("ResourceClaimName", resourceClaim.Name); + + var propertyName = "ResourceClaims"; + + if (existingResourceClaims.Count(x => x.Name == resourceClaim.Name) > 1) + { + if (_duplicateResources != null && resourceClaim.Name != null && !_duplicateResources.Contains(resourceClaim.Name)) + { + _duplicateResources.Add(resourceClaim.Name); + context.AddFailure(propertyName, "Only unique resource claims can be added. The following is a duplicate resource: '{ResourceClaimName}'"); + } + } + + if (!(resourceClaim.Create || resourceClaim.Delete || resourceClaim.Read || resourceClaim.Update || resourceClaim.ReadChanges)) + { + context.AddFailure(propertyName, "Only valid resources can be added. A resource must have at least one action associated with it to be added. The following is an invalid resource: '{ResourceClaimName}'"); + } + + var resources = dbResourceClaims[resourceClaim.Name!.ToLower()].ToList(); + if (resources.Count == 0) + { + context.AddFailure(propertyName, "This Claim Set contains a resource which is not in the system. Claimset Name: '{ClaimSetName}' Resource name: '{ResourceClaimName}'.\n"); + } + + if (resourceClaim.AuthStrategyOverridesForCRUD.Length != 0) + { + if (resourceClaim.AuthStrategyOverridesForCRUD.Length < 5) + context.AddFailure(propertyName, "Please provide a list of 5 elements for 'AuthStrategyOverridesForCRUD' in the Resource name: '{ResourceClaimName}'"); + } + + if (resourceClaim.Children.Count != 0) + { + foreach (var child in resourceClaim.Children) + { + var childResources = dbResourceClaims[child.Name!.ToLower()].ToList(); + if (childResources.Count != 0) + { + foreach (var childResource in childResources) + { + context.MessageFormatter.AppendArgument("ChildResource", childResource.Name); + if (childResource.ParentId == 0) + { + context.AddFailure(propertyName, "'{ChildResource}' can not be added as a child resource."); + } + + else if (!resources.Where(x => x is not null).Select(x => x.Id).Contains(childResource.ParentId)) + { + context.MessageFormatter.AppendArgument("CorrectParentResource", childResource.ParentName); + context.AddFailure(propertyName, "Child resource: '{ChildResource}' added to the wrong parent resource. Correct parent resource is: '{CorrectParentResource}'"); + } + } + } + Validate(dbResourceClaims, dbAuthStrategies, child, resourceClaim.Children, context, claimSetName); + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/FeatureConstants.cs b/Application/EdFi.Ods.AdminApi.V1/Features/FeatureConstants.cs new file mode 100644 index 000000000..2e4cdf8d8 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/FeatureConstants.cs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Features; + +public class FeatureConstants +{ + public const string VendorIdDescription = "Vendor/ company id"; + public const string VendorNameDescription = "Vendor/ company name"; + public const string VendorNamespaceDescription = "Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required."; + public const string VendorContactDescription = "Vendor contact name"; + public const string VendorContactEmailDescription = "Vendor contact email id"; + public const string ApplicationIdDescription = "Application id"; + public const string ApplicationNameDescription = "Application name"; + public const string ClaimSetIdDescription = "Claim set id"; + public const string ClaimSetNameDescription = "Claim set name"; + public const string ProfileIdDescription = "Profile id"; + public const string EducationOrganizationIdsDescription = "Education organization ids"; + public const string ResourceClaimsDescription = "Resource Claims"; + public const string ApplicationNameLengthValidationMessage = "The Application Name {ApplicationName} would be too long for Admin App to set up necessary Application records." + + " Consider shortening the name by {ExtraCharactersInName} character(s)."; + public const string ClaimSetNameValidationMessage = "Please provide a valid claim set name."; + public const string EdOrgIdsValidationMessage = "Please provide at least one education organization id."; + public const string VendorIdValidationMessage = "Please provide valid vendor id."; + public const string DeletedSuccessResponseDescription = "Resource was successfully deleted."; + public const string InternalServerErrorResponseDescription = "Internal server error. An unhandled error occurred on the server. See the response body for details."; + public const string BadRequestResponseDescription = "Bad Request. The request was invalid and cannot be completed. See the response body for details."; + public const string ClaimSetAlreadyExistsMessage = "A claim set with this name already exists in the database. Please enter a unique name."; + public const string ClaimSetNameMaxLengthMessage = "The claim set name must be less than 255 characters."; + public const string ClaimSetNotFound = "No such claim set exists in the database."; + public const string OdsInstanceIdDescription = "Ods Instance id"; + public const string OdsInstanceName = "Ods Instance name"; + public const string OdsInstanceInstanceType = "Ods Instance type"; + public const string OdsInstanceStatus = "Ods Instance status"; + public const string OdsInstanceIsExtended = "Ods Instance is extended"; + public const string OdsInstanceVersion = "Ods Instance version"; + public const string OdsInstanceConnectionString = "Ods Instance connection string"; + public const string OdsInstanceAlreadyExistsMessage = "An Ods instance with this name already exists in the database. Please enter a unique name."; + public const string OdsInstanceCantBeDeletedMessage = "There are some {Table} associated to this OdsInstance. Can not be deleted."; + public const string OdsInstanceConnectionStringInvalid = "The connection string is not valid."; +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/AddOdsInstance.cs b/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/AddOdsInstance.cs new file mode 100644 index 000000000..4d3656ce0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/AddOdsInstance.cs @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using FluentValidation; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.OdsInstances; + +public class AddOdsInstance : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPost(endpoints, "/odsInstances", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(201)) + .BuildForVersions(AdminApiVersions.V1); + } + + public async Task Handle(Validator validator, IAddOdsInstanceCommand addOdsInstanceCommand, IMapper mapper, AddOdsInstanceRequest request) + { + await validator.GuardAsync(request); + var addedOdsInstance = addOdsInstanceCommand.Execute(request); + var model = mapper.Map(addedOdsInstance); + return AdminApiResponse.Created(model, "odsInstance", $"/odsInstances/{model.OdsInstanceId}"); + } + + [SwaggerSchema(Title = "AddOdsInstanceRequest")] + public class AddOdsInstanceRequest : IAddOdsInstanceModel + { + [SwaggerSchema(Description = FeatureConstants.OdsInstanceName, Nullable = false)] + public string Name { get; set; } = string.Empty; + [SwaggerSchema(Description = FeatureConstants.OdsInstanceInstanceType, Nullable = true)] + public string InstanceType { get; set; } = string.Empty; + [SwaggerSchema(Description = FeatureConstants.OdsInstanceStatus, Nullable = true)] + public string Status { get; set; } = string.Empty; + [SwaggerSchema(Description = FeatureConstants.OdsInstanceIsExtended, Nullable = true)] + public bool? IsExtended { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceVersion, Nullable = true)] + public string Version { get; set; } = string.Empty; + } + + public class Validator : AbstractValidator + { + private readonly IGetOdsInstancesQuery _getOdsInstancesQuery; + + public Validator(IGetOdsInstancesQuery getOdsInstancesQuery) + { + _getOdsInstancesQuery = getOdsInstancesQuery; + + RuleFor(m => m.Name) + .NotEmpty() + .Must(BeAUniqueName) + .WithMessage(FeatureConstants.OdsInstanceAlreadyExistsMessage); + + RuleFor(m => m.Name) + .MaximumLength(100) + .When(m => !string.IsNullOrEmpty(m.Name)); + + RuleFor(m => m.InstanceType) + .NotEmpty(); + + RuleFor(m => m.InstanceType) + .MaximumLength(100) + .When(m => !string.IsNullOrEmpty(m.InstanceType)); + + RuleFor(m => m.Status) + .NotEmpty(); + + RuleFor(m => m.Status) + .MaximumLength(100) + .When(m => !string.IsNullOrEmpty(m.Status)); + + RuleFor(m => m.Version) + .NotEmpty(); + + RuleFor(m => m.Version) + .MaximumLength(20) + .When(m => !string.IsNullOrEmpty(m.Version)); + } + + private bool BeAUniqueName(string? name) + { + return _getOdsInstancesQuery.Execute().TrueForAll(x => x.Name != name); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/DeleteOdsInstance.cs b/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/DeleteOdsInstance.cs new file mode 100644 index 000000000..805482497 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/DeleteOdsInstance.cs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using FluentValidation; + +namespace EdFi.Ods.AdminApi.V1.Features.OdsInstances; + +public class DeleteOdsInstance : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapDelete(endpoints, "/odsInstances/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V1); + } + + internal async Task Handle(IDeleteOdsInstanceCommand deleteOdsInstanceCommand, Validator validator, int id) + { + var request = new Request() { Id = id }; + await validator.GuardAsync(request); + deleteOdsInstanceCommand.Execute(request.Id); + return await Task.FromResult(AdminApiResponse.Deleted("odsInstance")); + } + + public class Validator : AbstractValidator + { + private readonly IGetOdsInstanceQuery _getOdsInstanceQuery; + private readonly IGetApplicationsByOdsInstanceIdQuery _getApplicationByOdsInstanceIdQuery; + private OdsInstance? OdsInstanceEntity = null; + public Validator(IGetOdsInstanceQuery getOdsInstanceQuery, IGetApplicationsByOdsInstanceIdQuery getApplicationByOdsInstanceIdQuery) + { + _getOdsInstanceQuery = getOdsInstanceQuery; + _getApplicationByOdsInstanceIdQuery = getApplicationByOdsInstanceIdQuery; + + RuleFor(m => m.Id) + .Must(NotHaveApplicationsRelationships) + .WithMessage(FeatureConstants.OdsInstanceCantBeDeletedMessage) + .When(Exist); + } + + private bool Exist(Request request) + { + OdsInstanceEntity = _getOdsInstanceQuery.Execute(request.Id); + return true; + } + private bool NotHaveApplicationsRelationships(Request model, int odsIntanceId, ValidationContext context) + { + context.MessageFormatter.AppendArgument("Table", "Applications"); + List appList = _getApplicationByOdsInstanceIdQuery.Execute(odsIntanceId) ?? new List(); + return appList.Count == 0; + } + } + + public class Request + { + public int Id { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/EditOdsInstance.cs b/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/EditOdsInstance.cs new file mode 100644 index 000000000..a2970c5b0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/EditOdsInstance.cs @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using FluentValidation; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.OdsInstances; + +public class EditOdsInstance : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapPut(endpoints, "/odsInstances/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200)) + .BuildForVersions(AdminApiVersions.V1); + } + + public async Task Handle( + Validator validator, + IEditOdsInstanceCommand editOdsInstanceCommand, + IMapper mapper, + EditOdsInstanceRequest request, + int id + ) + { + request.OdsInstanceId = id; + await validator.GuardAsync(request); + var updatedOdsInstance = editOdsInstanceCommand.Execute(request); + var model = mapper.Map(updatedOdsInstance); + return AdminApiResponse.Updated(model, "odsInstance"); + } + + [SwaggerSchema(Title = "EditOdsInstanceRequest")] + public class EditOdsInstanceRequest : IEditOdsInstanceModel + { + [SwaggerSchema(Description = FeatureConstants.OdsInstanceIdDescription, Nullable = false)] + public int OdsInstanceId { get; set; } + + [SwaggerSchema(Description = FeatureConstants.OdsInstanceName, Nullable = false)] + public string Name { get; set; } = string.Empty; + + [SwaggerSchema(Description = FeatureConstants.OdsInstanceInstanceType, Nullable = true)] + public string InstanceType { get; set; } = string.Empty; + + [SwaggerSchema(Description = FeatureConstants.OdsInstanceStatus, Nullable = true)] + public string Status { get; set; } = string.Empty; + + [SwaggerSchema(Description = FeatureConstants.OdsInstanceIsExtended, Nullable = true)] + public bool? IsExtended { get; set; } + + [SwaggerSchema(Description = FeatureConstants.OdsInstanceVersion, Nullable = true)] + public string Version { get; set; } = string.Empty; + } + + public class Validator : AbstractValidator + { + private readonly IGetOdsInstancesQuery _getOdsInstancesQuery; + private readonly IGetOdsInstanceQuery _getOdsInstanceQuery; + + public Validator(IGetOdsInstancesQuery getOdsInstancesQuery, IGetOdsInstanceQuery getOdsInstanceQuery) + { + _getOdsInstancesQuery = getOdsInstancesQuery; + _getOdsInstanceQuery = getOdsInstanceQuery; + + RuleFor(m => m.OdsInstanceId) + .Must(id => id > 0) + .WithMessage("Please provide valid Ods instance Id."); + + RuleFor(m => m.Name) + .NotEmpty() + .Must(BeAUniqueName) + .WithMessage(FeatureConstants.OdsInstanceAlreadyExistsMessage) + .When(m => BeAnExistingOdsInstance(m.OdsInstanceId) && NameIsChanged(m)); + + RuleFor(m => m.Name).NotEmpty().MaximumLength(100).When(m => !string.IsNullOrEmpty(m.Name)); + + RuleFor(m => m.InstanceType) + .NotEmpty() + .MaximumLength(100) + .When(m => !string.IsNullOrEmpty(m.InstanceType)); + + RuleFor(m => m.Status).NotEmpty().MaximumLength(100).When(m => !string.IsNullOrEmpty(m.Status)); + + RuleFor(m => m.Version).NotEmpty().MaximumLength(20).When(m => !string.IsNullOrEmpty(m.Version)); + } + + private bool BeAnExistingOdsInstance(int id) + { + _getOdsInstanceQuery.Execute(id); + return true; + } + + private bool NameIsChanged(IEditOdsInstanceModel model) + { + return _getOdsInstanceQuery.Execute(model.OdsInstanceId).Name != model.Name; + } + + private bool BeAUniqueName(string? name) + { + return _getOdsInstancesQuery.Execute().TrueForAll(x => x.Name != name); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/OdsInstanceModel.cs b/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/OdsInstanceModel.cs new file mode 100644 index 000000000..8b708e7bc --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/OdsInstanceModel.cs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Swashbuckle.AspNetCore.Annotations; +using System.Text.Json.Serialization; + +namespace EdFi.Ods.AdminApi.V1.Features.OdsInstances; + +[SwaggerSchema(Title = "OdsInstance")] +public class OdsInstanceModel +{ + [JsonPropertyName("id")] + public int OdsInstanceId { get; set; } + public string? Name { get; set; } + public string? InstanceType { get; set; } + public string? Version { get; set; } + public bool? IsExtended { get; set; } + public string? Status { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/ReadOdsInstance.cs b/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/ReadOdsInstance.cs new file mode 100644 index 000000000..b7627e313 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/OdsInstances/ReadOdsInstance.cs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +namespace EdFi.Ods.AdminApi.V1.Features.OdsInstances; + +public class ReadOdsInstance : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstances", GetOdsInstances) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V1); + + AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstances/{id}", GetOdsInstance) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V1); + } + + internal Task GetOdsInstances(IGetOdsInstancesQuery getOdsInstancesQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams) + { + var odsInstances = mapper.Map>(getOdsInstancesQuery.Execute( + commonQueryParams)); + return Task.FromResult(AdminApiResponse>.Ok(odsInstances)); + } + + internal Task GetOdsInstance(IGetOdsInstanceQuery getOdsInstanceQuery, IMapper mapper, int id) + { + var odsInstance = getOdsInstanceQuery.Execute(id); + if (odsInstance == null) + { + throw new NotFoundException("odsInstance", id); + } + var model = mapper.Map(odsInstance); + return Task.FromResult(AdminApiResponse.Ok(model)); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Tenants/ReadTenants.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Tenants/ReadTenants.cs new file mode 100644 index 000000000..8c23db51b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Tenants/ReadTenants.cs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.V1.Features.Tenants; + +public class ReadTenants : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/tenants", GetTenantsAsync) + .BuildForVersions(AdminApiVersions.V1); + } + + public static IResult GetTenantsAsync(IOptions options, IOptions _appSettings) + { + const string ADMIN_DB_KEY = "EdFi_Admin"; + const string SECURITY_DB_KEY = "EdFi_Security"; + var _databaseEngine = options.Value.DatabaseEngine ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); + + var defaultTenant = new TenantModel() + { + TenantName = Common.Constants.Constants.DefaultTenantName, + ConnectionStrings = new TenantModelConnectionStrings + ( + edFiAdminConnectionString: _appSettings.Value.ConnectionStrings.First(p => p.Key == ADMIN_DB_KEY).Value, + edFiSecurityConnectionString: _appSettings.Value.ConnectionStrings.First(p => p.Key == SECURITY_DB_KEY).Value + ) + }; + + var adminHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase(_databaseEngine, defaultTenant.ConnectionStrings.EdFiAdminConnectionString); + var securityHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase(_databaseEngine, defaultTenant.ConnectionStrings.EdFiSecurityConnectionString); + + var response = new TenantsResponse + { + TenantName = defaultTenant.TenantName, + AdminConnectionString = new EdfiConnectionString() + { + host = adminHostAndDatabase.Host, + database = adminHostAndDatabase.Database + }, + SecurityConnectionString = new EdfiConnectionString() + { + host = securityHostAndDatabase.Host, + database = securityHostAndDatabase.Database + } + }; + return Results.Ok(response); + } +} + +public class TenantsResponse +{ + public string? TenantName { get; set; } + public EdfiConnectionString? AdminConnectionString { get; set; } + public EdfiConnectionString? SecurityConnectionString { get; set; } +} + +public class EdfiConnectionString +{ + public string? host { get; set; } + public string? database { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Tenants/TenantModel.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Tenants/TenantModel.cs new file mode 100644 index 000000000..90394a0eb --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Tenants/TenantModel.cs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Constants; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.Tenants; + +[SwaggerSchema] +public class TenantModel +{ + [SwaggerSchema(Description = Constants.TenantNameDescription, Nullable = false)] + public required string TenantName { get; set; } + + [SwaggerSchema(Description = Constants.TenantConnectionStringDescription, Nullable = false)] + public TenantModelConnectionStrings ConnectionStrings { get; set; } = new(); +} + +[SwaggerSchema] +public class TenantModelConnectionStrings +{ + public string EdFiSecurityConnectionString { get; set; } + public string EdFiAdminConnectionString { get; set; } + + public TenantModelConnectionStrings() + { + EdFiAdminConnectionString = string.Empty; + EdFiSecurityConnectionString = string.Empty; + } + + public TenantModelConnectionStrings(string edFiAdminConnectionString, string edFiSecurityConnectionString) + { + EdFiAdminConnectionString = edFiAdminConnectionString; + EdFiSecurityConnectionString = edFiSecurityConnectionString; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/AddVendor.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/AddVendor.cs new file mode 100644 index 000000000..fba6c0e46 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/AddVendor.cs @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using FluentValidation; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.Vendors; + +public class AddVendor : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapPost(endpoints, "/vendors", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(201)) + .BuildForVersions(AdminApiVersions.V1); + } + + public async Task Handle(Validator validator, AddVendorCommand addVendorCommand, IMapper mapper, AddVendorRequest request) + { + await validator.GuardAsync(request); + var addedVendor = addVendorCommand.Execute(request); + var model = mapper.Map(addedVendor); + return AdminApiResponse.Created(model, "Vendor", $"/vendors/{model.VendorId}"); + } + + [SwaggerSchema(Title = "AddVendorRequest")] + public class AddVendorRequest : IAddVendorModel + { + [SwaggerSchema(Description = FeatureConstants.VendorNameDescription, Nullable = false)] + public string Company { get; set; } = string.Empty; + + [SwaggerSchema(Description = FeatureConstants.VendorNamespaceDescription, Nullable = false)] + public string NamespacePrefixes { get; set; } = string.Empty; + + [SwaggerSchema(Description = FeatureConstants.VendorContactDescription, Nullable = false)] + public string ContactName { get; set; } = string.Empty; + + [SwaggerSchema(Description = FeatureConstants.VendorContactEmailDescription, Nullable = false)] + public string ContactEmailAddress { get; set; } = string.Empty; + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(m => m.Company).NotEmpty(); + RuleFor(m => m.Company) + .Must(name => !VendorExtensions.IsSystemReservedVendorName(name)) + .WithMessage(p => $"'{p.Company}' is a reserved name and may not be used. Please choose another name."); + + RuleFor(m => m.ContactName).NotEmpty(); + RuleFor(m => m.ContactEmailAddress).NotEmpty().EmailAddress(); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/DeleteVendor.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/DeleteVendor.cs new file mode 100644 index 000000000..db1d2c182 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/DeleteVendor.cs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; + +namespace EdFi.Ods.AdminApi.V1.Features.Vendors; + +public class DeleteVendor : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapDelete(endpoints, "/vendors/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V1); + } + + public Task Handle(DeleteVendorCommand deleteVendorCommand, int id) + { + deleteVendorCommand.Execute(id); + return Task.FromResult(AdminApiResponse.Deleted("Vendor")); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/EditVendor.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/EditVendor.cs new file mode 100644 index 000000000..dd04c5e96 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/EditVendor.cs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using FluentValidation; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.Vendors; + +public class EditVendor : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPut(endpoints, "/vendors/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V1); + } + + public async Task Handle(EditVendorCommand editVendorCommand, IMapper mapper, + Validator validator, EditVendorRequest request, int id) + { + request.VendorId = id; + await validator.GuardAsync(request); + var updatedVendor = editVendorCommand.Execute(request); + var model = mapper.Map(updatedVendor); + return AdminApiResponse.Updated(model, "Vendor"); + } + + [SwaggerSchema(Title = "EditVendorRequest")] + public class EditVendorRequest : IEditVendor + { + [SwaggerSchema(Description = FeatureConstants.VendorIdDescription, Nullable = false)] + public int VendorId { get; set; } + + [SwaggerSchema(Description = FeatureConstants.VendorNameDescription, Nullable = false)] + public string Company { get; set; } = string.Empty; + + [SwaggerSchema(Description = FeatureConstants.VendorNamespaceDescription, Nullable = false)] + public string? NamespacePrefixes { get; set; } + + [SwaggerSchema(Description = FeatureConstants.VendorContactDescription, Nullable = false)] + public string ContactName { get; set; } = string.Empty; + + [SwaggerSchema(Description = FeatureConstants.VendorContactEmailDescription, Nullable = false)] + public string ContactEmailAddress { get; set; } = string.Empty; + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(m => m.VendorId).Must(id => id > 0).WithMessage("Please provide valid Vendor Id."); + RuleFor(m => m.Company).NotEmpty(); + RuleFor(m => m.Company) + .Must(name => !VendorExtensions.IsSystemReservedVendorName(name)) + .WithMessage(p => $"'{p.Company}' is a reserved name and may not be used. Please choose another name."); + + RuleFor(m => m.ContactName).NotEmpty(); + RuleFor(m => m.ContactEmailAddress).NotEmpty().EmailAddress(); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/ReadVendor.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/ReadVendor.cs new file mode 100644 index 000000000..a4519cd1e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/ReadVendor.cs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +namespace EdFi.Ods.AdminApi.V1.Features.Vendors; + +public class ReadVendor : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/vendors", GetVendors) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V1); + + AdminApiEndpointBuilder.MapGet(endpoints, "/vendors/{id}", GetVendor) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V1); + } + + internal Task GetVendors(IGetVendorsQuery getVendorsQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams) + { + var vendorList = mapper.Map>(getVendorsQuery.Execute(commonQueryParams)); + return Task.FromResult(AdminApiResponse>.Ok(vendorList)); + } + + internal Task GetVendor(IGetVendorByIdQuery getVendorByIdQuery, IMapper mapper, int id) + { + var vendor = getVendorByIdQuery.Execute(id); + if (vendor == null) + { + throw new NotFoundException("vendor", id); + } + var model = mapper.Map(vendor); + return Task.FromResult(AdminApiResponse.Ok(model)); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/VendorModel.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/VendorModel.cs new file mode 100644 index 000000000..688f1d96e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Vendors/VendorModel.cs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.Vendors; + +[SwaggerSchema(Title = "Vendor")] +public class VendorModel +{ + public int? VendorId { get; set; } + public string? Company { get; set; } + public string? NamespacePrefixes { get; set; } + public string? ContactName { get; set; } + public string? ContactEmailAddress { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/AutoMapper/AdminApiMappingProfile.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/AutoMapper/AdminApiMappingProfile.cs new file mode 100644 index 000000000..b641e4be4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/AutoMapper/AdminApiMappingProfile.cs @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using Profile = AutoMapper.Profile; +using EdFi.Ods.AdminApi.V1.Features.Vendors; +using EdFi.Ods.AdminApi.V1.Features.Applications; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.V1.Features.ClaimSets; +using EdFi.Ods.AdminApi.V1.Infrastructure.Helpers; +using ClaimSetEditor = EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using EdFi.Ods.AdminApi.V1.Features.OdsInstances; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.AutoMapper; + +public class AdminApiMappingProfile : Profile +{ + public AdminApiMappingProfile() + { + CreateMap() + .ForMember(dst => dst.Company, opt => opt.MapFrom(src => src.VendorName)) + .ForMember(dst => dst.ContactName, opt => opt.MapFrom(src => src.ContactName())) + .ForMember(dst => dst.ContactEmailAddress, opt => opt.MapFrom(src => src.ContactEmail())) + .ForMember(dst => dst.NamespacePrefixes, opt => opt.MapFrom(src => src.VendorNamespacePrefixes != null ? src.VendorNamespacePrefixes.ToCommaSeparated() : null)); + + CreateMap() + .ForMember(dst => dst.Company, opt => opt.MapFrom(src => src.VendorName)) + .ForMember(dst => dst.ContactName, opt => opt.MapFrom(src => src.ContactName())) + .ForMember(dst => dst.ContactEmailAddress, opt => opt.MapFrom(src => src.ContactEmail())) + .ForMember(dst => dst.NamespacePrefixes, opt => opt.MapFrom(src => src.VendorNamespacePrefixes != null ? src.VendorNamespacePrefixes.ToCommaSeparated() : null)); + + CreateMap() + .ForMember(dst => dst.EducationOrganizationIds, opt => opt.MapFrom(src => src.EducationOrganizationIds())) + .ForMember(dst => dst.ProfileName, opt => opt.MapFrom(src => src.ProfileName())) + .ForMember(dst => dst.OdsInstanceId, opt => opt.MapFrom(src => src.OdsInstance != null ? src.OdsInstance.OdsInstanceId : 0)) + .ForMember(dst => dst.OdsInstanceName, opt => opt.MapFrom(src => src.OdsInstanceName())) + .ForMember(dst => dst.VendorId, opt => opt.MapFrom(src => src.VendorId())) + .ForMember(dst => dst.Profiles, opt => opt.MapFrom(src => src.Profiles())); + + CreateMap() + .ForMember(dst => dst.ApplicationId, opt => opt.MapFrom(src => src.ApplicationId)) + .ForMember(dst => dst.Key, opt => opt.MapFrom(src => src.Key)) + .ForMember(dst => dst.Secret, opt => opt.MapFrom(src => src.Secret)); + + CreateMap() + .ForMember(dst => dst.ApplicationId, opt => opt.MapFrom(src => src.Application.ApplicationId)) + .ForMember(dst => dst.Key, opt => opt.MapFrom(src => src.Key)) + .ForMember(dst => dst.Secret, opt => opt.MapFrom(src => src.Secret)); + + CreateMap() + //.ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.InstanceType, opt => opt.MapFrom(src => src.InstanceType)) + .ForMember(dst => dst.Version, opt => opt.MapFrom(src => src.Version)) + .ForMember(dst => dst.IsExtended, opt => opt.MapFrom(src => src.IsExtended)) + .ForMember(dst => dst.Status, opt => opt.MapFrom(src => src.Status)); + + CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.IsSystemReserved, opt => opt.MapFrom(src => !src.IsEditable)); + + CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.IsSystemReserved, opt => opt.MapFrom(src => !src.IsEditable)); + + CreateMap() + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.Read, opt => opt.MapFrom(src => src.Read)) + .ForMember(dst => dst.Update, opt => opt.MapFrom(src => src.Update)) + .ForMember(dst => dst.Create, opt => opt.MapFrom(src => src.Create)) + .ForMember(dst => dst.Delete, opt => opt.MapFrom(src => src.Delete)) + .ForMember(dst => dst.ReadChanges, opt => opt.MapFrom(src => src.ReadChanges)) + .ForMember(dst => dst.AuthStrategyOverridesForCRUD, opt => opt.MapFrom(src => src.AuthStrategyOverridesForCRUD)) + .ForMember(dst => dst.DefaultAuthStrategiesForCRUD, opt => opt.MapFrom(src => src.DefaultAuthStrategiesForCRUD)) + .ForMember(dst => dst.Children, opt => opt.MapFrom(src => src.Children)); + + CreateMap() + .ForMember(dst => dst.AuthStrategyId, opt => opt.MapFrom(src => src.AuthStrategyId)) + .ForMember(dst => dst.AuthStrategyName, opt => opt.MapFrom(src => src.AuthStrategyName)) + .ForMember(dst => dst.DisplayName, opt => opt.MapFrom(src => src.DisplayName)) + .ForMember(dst => dst.IsInheritedFromParent, opt => opt.MapFrom(src => src.IsInheritedFromParent)); + + CreateMap() + .ForMember(dst => dst.AuthStrategyId, opt => opt.MapFrom(src => src.AuthStrategyId)) + .ForMember(dst => dst.AuthStrategyName, opt => opt.MapFrom(src => src.AuthStrategyName)) + .ForMember(dst => dst.DisplayName, opt => opt.MapFrom(src => src.DisplayName)) + .ForMember(dst => dst.IsInheritedFromParent, opt => opt.MapFrom(src => src.IsInheritedFromParent)); + + CreateMap() + .ForMember(dst => dst.AuthStrategyName, opt => opt.MapFrom(src => src.AuthorizationStrategyName)) + .ForMember(dst => dst.AuthStrategyId, opt => opt.MapFrom(src => src.AuthorizationStrategyId)) + .ForMember(dst => dst.IsInheritedFromParent, opt => opt.Ignore()); + + CreateMap() + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.Read, opt => opt.MapFrom(src => src.Read)) + .ForMember(dst => dst.Update, opt => opt.MapFrom(src => src.Update)) + .ForMember(dst => dst.Create, opt => opt.MapFrom(src => src.Create)) + .ForMember(dst => dst.Delete, opt => opt.MapFrom(src => src.Delete)) + .ForMember(dst => dst.ReadChanges, opt => opt.MapFrom(src => src.ReadChanges)) + .ForMember(dst => dst.AuthStrategyOverridesForCRUD, opt => opt.MapFrom(src => src.AuthStrategyOverridesForCRUD)) + .ForMember(dst => dst.DefaultAuthStrategiesForCRUD, opt => opt.MapFrom(src => src.DefaultAuthStrategiesForCRUD)) + .ForMember(dst => dst.Children, opt => opt.MapFrom(src => src.Children)); + + CreateMap() + .ForMember(dst => dst.AuthorizationStrategies, opt => opt.MapFrom(src => src.AuthorizationStrategies)).ReverseMap(); + + CreateMap() + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.Read, opt => opt.MapFrom(src => src.Read)) + .ForMember(dst => dst.Update, opt => opt.MapFrom(src => src.Update)) + .ForMember(dst => dst.Create, opt => opt.MapFrom(src => src.Create)) + .ForMember(dst => dst.Delete, opt => opt.MapFrom(src => src.Delete)) + .ForMember(dst => dst.ReadChanges, opt => opt.MapFrom(src => src.ReadChanges)) + .ForMember(dst => dst.AuthStrategyOverridesForCRUD, opt => opt.MapFrom(src => src.AuthStrategyOverridesForCRUD)) + .ForMember(dst => dst.Children, opt => opt.MapFrom(src => src.Children)); + + CreateMap() + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.Read, opt => opt.MapFrom(src => src.Read)) + .ForMember(dst => dst.Update, opt => opt.MapFrom(src => src.Update)) + .ForMember(dst => dst.Create, opt => opt.MapFrom(src => src.Create)) + .ForMember(dst => dst.Delete, opt => opt.MapFrom(src => src.Delete)) + .ForMember(dst => dst.ReadChanges, opt => opt.MapFrom(src => src.ReadChanges)) + .ForMember(dst => dst.AuthStrategyOverridesForCRUD, opt => opt.MapFrom(src => src.AuthStrategyOverridesForCRUD)) + .ForMember(dst => dst.Children, opt => opt.MapFrom(src => src.Children)); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/AddApplicationCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/AddApplicationCommand.cs new file mode 100644 index 000000000..12d181a16 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/AddApplicationCommand.cs @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; + +public interface IAddApplicationCommand +{ + AddApplicationResult Execute(IAddApplicationModel applicationModel); +} + +public class AddApplicationCommand(IUsersContext usersContext, InstanceContext instanceContext) : IAddApplicationCommand +{ + private readonly IUsersContext _usersContext = usersContext; + private readonly InstanceContext _instanceContext = instanceContext; + + public AddApplicationResult Execute(IAddApplicationModel applicationModel) + { + var profile = applicationModel.ProfileId.HasValue + ? _usersContext.Profiles.SingleOrDefault(p => p.ProfileId == applicationModel.ProfileId.Value) + : null; + + var vendor = _usersContext.Vendors.Include(x => x.Users) + .Single(v => v.VendorId == applicationModel.VendorId); + + OdsInstance? odsInstance; + + if (_instanceContext != null && !string.IsNullOrEmpty(_instanceContext.Name)) + { + odsInstance = _usersContext.OdsInstances.AsEnumerable().FirstOrDefault(x => + x.Name.Equals(_instanceContext.Name, StringComparison.InvariantCultureIgnoreCase)); + } + else + { + odsInstance = _usersContext.OdsInstances.FirstOrDefault(o => o.OdsInstanceId == applicationModel.OdsInstanceId); + } + + var user = vendor.Users.FirstOrDefault(); + + var apiClient = new ApiClient(true) + { + Name = applicationModel.ApplicationName, + IsApproved = true, + UseSandbox = false, + KeyStatus = "Active", + User = user + }; + + var applicationEdOrgs = applicationModel.EducationOrganizationIds == null + ? [] + : applicationModel.EducationOrganizationIds.Select(id => new ApplicationEducationOrganization + { + Clients = [apiClient], + EducationOrganizationId = id + }); + + var application = new Application + { + ApplicationName = applicationModel.ApplicationName, + ApiClients = [apiClient], + ApplicationEducationOrganizations = new List(applicationEdOrgs), + ClaimSetName = applicationModel.ClaimSetName, + Profiles = [], + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri, + OdsInstance = odsInstance + }; + + if (profile != null) + { + application.Profiles.Add(profile); + } + + _usersContext.Applications.Add(application); + _usersContext.SaveChanges(); + + return new AddApplicationResult + { + ApplicationId = application.ApplicationId, + Key = apiClient.Key, + Secret = apiClient.Secret + }; + } +} + +public interface IAddApplicationModel +{ + string ApplicationName { get; } + int VendorId { get; } + string? ClaimSetName { get; } + int? ProfileId { get; } + int? OdsInstanceId { get; } + IEnumerable? EducationOrganizationIds { get; } +} + +public class AddApplicationResult +{ + public int ApplicationId { get; set; } + public string? Key { get; set; } + public string? Secret { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/AddOdsInstanceCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/AddOdsInstanceCommand.cs new file mode 100644 index 000000000..b1239d632 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/AddOdsInstanceCommand.cs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; + +public interface IAddOdsInstanceCommand +{ + OdsInstance Execute(IAddOdsInstanceModel newOdsInstance); +} + +public class AddOdsInstanceCommand : IAddOdsInstanceCommand +{ + private readonly IUsersContext _context; + + public AddOdsInstanceCommand(IUsersContext context) + { + _context = context; + } + + public OdsInstance Execute(IAddOdsInstanceModel newOdsInstance) + { + var odsInstance = new OdsInstance + { + Name = newOdsInstance.Name, + InstanceType = newOdsInstance.InstanceType, + Status = newOdsInstance.Status, + IsExtended = newOdsInstance.IsExtended ?? false, + Version = newOdsInstance.Version, + }; + _context.OdsInstances.Add(odsInstance); + _context.SaveChanges(); + return odsInstance; + } +} + +public interface IAddOdsInstanceModel +{ + string Name { get; } + string InstanceType { get; } + string Status { get; set; } + bool? IsExtended { get; } + string Version { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/AddVendorCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/AddVendorCommand.cs new file mode 100644 index 000000000..e48af4d05 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/AddVendorCommand.cs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using VendorUser = EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models.User; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; + +public class AddVendorCommand +{ + private readonly IUsersContext _context; + + public AddVendorCommand(IUsersContext context) + { + _context = context; + } + + public Vendor Execute(IAddVendorModel newVendor) + { + var namespacePrefixes = newVendor.NamespacePrefixes?.Split(",") + .Where(namespacePrefix => !string.IsNullOrWhiteSpace(namespacePrefix)) + .Select(namespacePrefix => new VendorNamespacePrefix + { + NamespacePrefix = namespacePrefix.Trim(), + Vendor = new Vendor() + }) + .ToList(); + + var vendor = new Vendor + { + VendorName = newVendor.Company.Trim(), + VendorNamespacePrefixes = namespacePrefixes + }; + + var user = new VendorUser + { + FullName = newVendor.ContactName.Trim(), + Email = newVendor.ContactEmailAddress.Trim(), + Vendor = vendor + }; + + vendor.Users.Add(user); + + _context.Vendors.Add(vendor); + _context.SaveChanges(); + return vendor; + } +} + +public interface IAddVendorModel +{ + string Company { get; } + string? NamespacePrefixes { get; } + string ContactName { get; } + string ContactEmailAddress { get; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/DeleteApplicationCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/DeleteApplicationCommand.cs new file mode 100644 index 000000000..00b97c071 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/DeleteApplicationCommand.cs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; + +public interface IDeleteApplicationCommand +{ + void Execute(int id); +} + +public class DeleteApplicationCommand : IDeleteApplicationCommand +{ + private readonly IUsersContext _context; + + public DeleteApplicationCommand(IUsersContext context) + { + _context = context; + } + + public void Execute(int id) + { + var application = _context.Applications + .Include(a => a.ApiClients).ThenInclude(c => c.ClientAccessTokens) + .Include(a => a.ApplicationEducationOrganizations) + .SingleOrDefault(a => a.ApplicationId == id) ?? throw new NotFoundException("application", id); + + if (application != null && application.Vendor != null && application.Vendor.IsSystemReservedVendor()) + { + throw new Exception("This Application is required for proper system function and may not be modified"); + } + + if (application == null) + { + return; + } + + application.ApiClients.ToList().ForEach(a => + { + a.ClientAccessTokens.ToList().ForEach(t => _context.ClientAccessTokens.Remove(t)); + _context.Clients.Remove(a); + }); + + application.ApplicationEducationOrganizations.ToList().ForEach(o => _context.ApplicationEducationOrganizations.Remove(o)); + + _context.Applications.Remove(application); + _context.SaveChanges(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/DeleteOdsInstanceCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/DeleteOdsInstanceCommand.cs new file mode 100644 index 000000000..36f2cc9c8 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/DeleteOdsInstanceCommand.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; + +public interface IDeleteOdsInstanceCommand +{ + void Execute(int id); +} + +public class DeleteOdsInstanceCommand : IDeleteOdsInstanceCommand +{ + private readonly IUsersContext _context; + + public DeleteOdsInstanceCommand(IUsersContext context) + { + _context = context; + } + + public void Execute(int id) + { + var odsInstance = _context.OdsInstances.SingleOrDefault(v => v.OdsInstanceId == id) ?? throw new NotFoundException("odsInstance", id); + _context.OdsInstances.Remove(odsInstance); + _context.SaveChanges(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/DeleteVendorCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/DeleteVendorCommand.cs new file mode 100644 index 000000000..556f127b1 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/DeleteVendorCommand.cs @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; + +public class DeleteVendorCommand +{ + private readonly IUsersContext _context; + private readonly IDeleteApplicationCommand _deleteApplicationCommand; + + public DeleteVendorCommand(IUsersContext context, IDeleteApplicationCommand deleteApplicationCommand) + { + _context = context; + _deleteApplicationCommand = deleteApplicationCommand; + } + + public void Execute(int id) + { + var vendor = _context.Vendors + .Include(x => x.Applications) + .Include(x => x.Users) + .SingleOrDefault(v => v.VendorId == id) ?? throw new NotFoundException("vendor", id); + + if (vendor.IsSystemReservedVendor()) + { + throw new Exception("This Vendor is required for proper system function and may not be deleted"); + } + + foreach (var application in vendor.Applications.ToList()) + { + _deleteApplicationCommand.Execute(application.ApplicationId); + } + + foreach (var user in vendor.Users.ToList()) + { + _context.Users.Remove(user); + } + + _context.Vendors.Remove(vendor); + _context.SaveChanges(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/EditApplicationCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/EditApplicationCommand.cs new file mode 100644 index 000000000..de9675d23 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/EditApplicationCommand.cs @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common.Utils.Extensions; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; + +public interface IEditApplicationCommand +{ + Application Execute(IEditApplicationModel model); +} + +public class EditApplicationCommand(IUsersContext context) : IEditApplicationCommand +{ + private readonly IUsersContext _context = context; + + public Application Execute(IEditApplicationModel model) + { + var application = _context.Applications + .Include(a => a.Vendor) + .Include(a => a.ApplicationEducationOrganizations) + .Include(a => a.ApiClients) + .Include(a => a.Profiles) + .SingleOrDefault(a => a.ApplicationId == model.ApplicationId) ?? throw new NotFoundException("application", model.ApplicationId); + + if (application.Vendor != null && application.Vendor.IsSystemReservedVendor()) + { + throw new Exception("This Application is required for proper system function and may not be modified"); + } + + var newVendor = _context.Vendors.Single(v => v.VendorId == model.VendorId); + var newProfile = model.ProfileId.HasValue + ? _context.Profiles.Single(p => p.ProfileId == model.ProfileId.Value) + : null; + + var apiClient = application.ApiClients.Single(); + apiClient.Name = model.ApplicationName; + + application.ApplicationName = model.ApplicationName; + application.ClaimSetName = model.ClaimSetName; + application.Vendor = newVendor; + + application.ApplicationEducationOrganizations ??= []; + + // Quick and dirty: simply remove all existing links to ApplicationEducationOrganizations... + application.ApplicationEducationOrganizations.ToList().ForEach(x => _context.ApplicationEducationOrganizations.Remove(x)); + application.ApplicationEducationOrganizations.Clear(); + // ... and now create the new proper list. + model.EducationOrganizationIds?.ForEach(id => application.ApplicationEducationOrganizations.Add(application.CreateApplicationEducationOrganization(id))); + + application.Profiles ??= []; + + application.Profiles.Clear(); + + if (newProfile != null) + { + application.Profiles.Add(newProfile); + } + + _context.SaveChanges(); + return application; + } +} + +public interface IEditApplicationModel +{ + int ApplicationId { get; } + string ApplicationName { get; } + int VendorId { get; } + string? ClaimSetName { get; } + int? ProfileId { get; } + int? OdsInstanceId { get; } + IEnumerable? EducationOrganizationIds { get; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/EditOdsInstanceCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/EditOdsInstanceCommand.cs new file mode 100644 index 000000000..11ec329c2 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/EditOdsInstanceCommand.cs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +public interface IEditOdsInstanceCommand +{ + OdsInstance Execute(IEditOdsInstanceModel changedOdsInstanceData); +} + +public class EditOdsInstanceCommand : IEditOdsInstanceCommand +{ + private readonly IUsersContext _context; + + public EditOdsInstanceCommand(IUsersContext context) + { + _context = context; + } + + public OdsInstance Execute(IEditOdsInstanceModel changedOdsInstanceData) + { + var odsInstance = _context.OdsInstances.SingleOrDefault(v => v.OdsInstanceId == changedOdsInstanceData.OdsInstanceId) ?? + throw new NotFoundException("odsInstance", changedOdsInstanceData.OdsInstanceId); + + odsInstance.Name = changedOdsInstanceData.Name; + odsInstance.InstanceType = changedOdsInstanceData.InstanceType; + odsInstance.IsExtended = changedOdsInstanceData.IsExtended ?? false; + odsInstance.Status = changedOdsInstanceData.Status; + odsInstance.Version = changedOdsInstanceData.Version; + + _context.SaveChanges(); + return odsInstance; + } +} + +public interface IEditOdsInstanceModel +{ + public int OdsInstanceId { get; set; } + string Name { get; } + string InstanceType { get; } + string Status { get; set; } + bool? IsExtended { get; } + string Version { get; set; } +} + diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/EditVendorCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/EditVendorCommand.cs new file mode 100644 index 000000000..0ab906d7b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/EditVendorCommand.cs @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using Microsoft.EntityFrameworkCore; +using VendorUser = EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models.User; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; + +public class EditVendorCommand +{ + private readonly IUsersContext _context; + + public EditVendorCommand(IUsersContext context) + { + _context = context; + } + + public Vendor Execute(IEditVendor changedVendorData) + { + var vendor = _context.Vendors + .Include(x => x.VendorNamespacePrefixes) + .Include(x => x.Users) + .SingleOrDefault(v => v.VendorId == changedVendorData.VendorId) ?? throw new NotFoundException("vendor", changedVendorData.VendorId); + + if (vendor.IsSystemReservedVendor()) + { + throw new Exception("This vendor is required for proper system function and may not be modified."); + } + + vendor.VendorName = changedVendorData.Company; + + if (vendor.VendorNamespacePrefixes != null && vendor.VendorNamespacePrefixes.Any()) + { + foreach (var vendorNamespacePrefix in vendor.VendorNamespacePrefixes.ToList()) + { + _context.VendorNamespacePrefixes.Remove(vendorNamespacePrefix); + } + } + + var namespacePrefixes = changedVendorData.NamespacePrefixes?.Split(",") + .Where(namespacePrefix => !string.IsNullOrWhiteSpace(namespacePrefix)) + .Select(namespacePrefix => new VendorNamespacePrefix + { + NamespacePrefix = namespacePrefix.Trim(), + Vendor = vendor + }); + + foreach (var namespacePrefix in namespacePrefixes ?? Enumerable.Empty()) + { + _context.VendorNamespacePrefixes.Add(namespacePrefix); + } + + if (vendor.Users?.FirstOrDefault() != null) + { + vendor.Users.First().FullName = changedVendorData.ContactName; + vendor.Users.First().Email = changedVendorData.ContactEmailAddress; + } + + else + { + var vendorContact = new VendorUser + { + Vendor = vendor, + FullName = changedVendorData.ContactName, + Email = changedVendorData.ContactEmailAddress + }; + vendor.Users = new List { vendorContact }; + } + + _context.SaveChanges(); + return vendor; + } +} + +public interface IEditVendor +{ + int VendorId { get; set; } + string Company { get; set; } + string? NamespacePrefixes { get; set; } + string ContactName { get; set; } + string ContactEmailAddress { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/RegenerateApiClientSecretCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/RegenerateApiClientSecretCommand.cs new file mode 100644 index 000000000..29555811e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/RegenerateApiClientSecretCommand.cs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Commands; + +public class RegenerateApiClientSecretCommand +{ + private readonly IUsersContext _context; + + public RegenerateApiClientSecretCommand(IUsersContext context) + { + _context = context; + } + + public RegenerateApiClientSecretResult Execute(int applicationId) + { + var application = _context.Applications + .Include(x => x.ApiClients) + .SingleOrDefault(a => a.ApplicationId == applicationId); + if (application == null) + { + throw new NotFoundException("application", applicationId); + } + + var apiClient = application.ApiClients.First(); + + apiClient.GenerateSecret(); + apiClient.SecretIsHashed = false; + _context.SaveChanges(); + + return new RegenerateApiClientSecretResult + { + Key = apiClient.Key, + Secret = apiClient.Secret, + Application = application + }; + } +} + +public class RegenerateApiClientSecretResult +{ + public string? Key { get; set; } + public string? Secret { get; set; } + public Application Application { get; set; } = new() { OperationalContextUri = string.Empty }; +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/ValidationConstants.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/ValidationConstants.cs new file mode 100644 index 000000000..a1fcd43fc --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Commands/ValidationConstants.cs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Commands; + +public static class ValidationConstants +{ + public static int MaximumApplicationNameLength = 50; +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs new file mode 100644 index 000000000..8766e2205 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/EntityFrameworkCoreDatabaseModelBuilderExtensions.cs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database; + +public static class EntityFrameworkCoreDatabaseModelBuilderExtensions +{ + public static void ApplyDatabaseServerSpecificConventions(this ModelBuilder modelBuilder, string databaseEngine) + { + if ("SqlServer".Equals(databaseEngine, StringComparison.InvariantCultureIgnoreCase)) + { + return; + } + + foreach (var entity in modelBuilder.Model.GetEntityTypes()) + { + if (entity is null) + throw new InvalidOperationException("Entity should not be null"); + var tableName = entity.GetTableName() ?? throw new InvalidOperationException($"Entity of type {entity.GetType()} has a null table name"); + + entity.SetTableName(tableName.ToLowerInvariant()); + + foreach (var property in entity.GetProperties()) + { + var tableId = StoreObjectIdentifier.Table(tableName); + var columnName = property.GetColumnName(tableId) ?? property.GetDefaultColumnName(tableId); + if (columnName != null) + property.SetColumnName(columnName.ToLowerInvariant()); + } + + foreach (var key in entity.GetKeys()) + key.SetName(key.GetName()?.ToLowerInvariant()); + + foreach (var key in entity.GetForeignKeys()) + key.SetConstraintName(key.GetConstraintName()?.ToLowerInvariant()); + + foreach (var index in entity.GetIndexes()) + index.SetDatabaseName(index.GetDatabaseName()?.ToLowerInvariant()); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetApplicationByIdQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetApplicationByIdQuery.cs new file mode 100644 index 000000000..f40a4e76c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetApplicationByIdQuery.cs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +public class GetApplicationByIdQuery +{ + private readonly IUsersContext _context; + + public GetApplicationByIdQuery(IUsersContext context) + { + _context = context; + } + + public Application Execute(int applicationId) + { + var application = _context.Applications + .Include(x => x.Vendor) + .Include(x => x.ApplicationEducationOrganizations) + .Include(x => x.Profiles) + .Include(x => x.OdsInstance) + .Include(x => x.ApiClients) + .SingleOrDefault(app => app.ApplicationId == applicationId); + if (application == null) + { + throw new NotFoundException("application", applicationId); + } + + return application; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetApplicationsByOdsInstanceIdQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetApplicationsByOdsInstanceIdQuery.cs new file mode 100644 index 000000000..42ceed8da --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetApplicationsByOdsInstanceIdQuery.cs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +public interface IGetApplicationsByOdsInstanceIdQuery +{ + List Execute(int odsInstanceId); +} + +public class GetApplicationsByOdsInstanceIdQuery : IGetApplicationsByOdsInstanceIdQuery +{ + private readonly IUsersContext _context; + + public GetApplicationsByOdsInstanceIdQuery(IUsersContext context) + { + _context = context; + } + + public List Execute(int odsInstanceId) + { + var applications = _context.Applications + .Include(aco => aco.OdsInstance) + .Include(aco => aco.ApplicationEducationOrganizations) + .Include(api => api.Profiles) + .Include(api => api.Vendor) + .Where(a => a.OdsInstance == null || a.OdsInstance.OdsInstanceId == odsInstanceId) + .ToList(); + + if (!applications.Any() && _context.OdsInstances.Find(odsInstanceId) == null) + { + throw new NotFoundException("odsinstance", odsInstanceId); + } + + return applications; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetApplicationsByVendorIdQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetApplicationsByVendorIdQuery.cs new file mode 100644 index 000000000..a3b621f7a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetApplicationsByVendorIdQuery.cs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +public class GetApplicationsByVendorIdQuery +{ + private readonly IUsersContext _context; + + public GetApplicationsByVendorIdQuery(IUsersContext context) + { + _context = context; + } + + public List Execute(int vendorid) + { + var applications = _context.Applications + .Include(x=> x.Profiles) + .Include(x => x.OdsInstance) + .Include(x => x.ApplicationEducationOrganizations) + .Include(x => x.Vendor) + .Where(a => a.Vendor != null && a.Vendor.VendorId == vendorid) + .ToList(); + + if (!applications.Any() && _context.Vendors.Find(vendorid) == null) + { + throw new NotFoundException("vendor", vendorid); + } + + return applications; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationsQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetApplicationsQuery.cs similarity index 60% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationsQuery.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetApplicationsQuery.cs index 649627ee5..3af54180b 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationsQuery.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetApplicationsQuery.cs @@ -3,14 +3,15 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Admin.DataAccess.Contexts; -using EdFi.Admin.DataAccess.Models; -using EdFi.Ods.AdminApi.Helpers; -using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; public interface IGetApplicationsQuery { List Execute(); @@ -31,26 +32,26 @@ public GetApplicationsQuery(IUsersContext context, IOptions options public List Execute() { return _context.Applications - .Include(ap => ap.Vendor).ThenInclude(ap => ap.VendorNamespacePrefixes) - .Include(ap => ap.Vendor).ThenInclude(ap => ap.Users) + .Include(ap => ap.Vendor!).ThenInclude(ap => ap.VendorNamespacePrefixes) + .Include(ap => ap.Vendor!).ThenInclude(ap => ap.Users) .Include(ap => ap.Profiles) .Include(ap => ap.OdsInstance) .Include(ap => ap.ApplicationEducationOrganizations) - .OrderBy(v => v.Vendor.VendorName) - .Where(v => !VendorExtensions.ReservedNames.Contains(v.Vendor.VendorName.Trim())) + .OrderBy(v => v.Vendor!.VendorName) + .Where(v => v.Vendor != null && v.Vendor.VendorName != null && !VendorExtensions.ReservedNames.Contains(v.Vendor.VendorName.Trim())) .ToList(); } public List Execute(CommonQueryParams commonQueryParams) { return _context.Applications - .Include(ap => ap.Vendor).ThenInclude(ap => ap.VendorNamespacePrefixes) - .Include(ap => ap.Vendor).ThenInclude(ap => ap.Users) + .Include(ap => ap.Vendor!).ThenInclude(ap => ap.VendorNamespacePrefixes) + .Include(ap => ap.Vendor!).ThenInclude(ap => ap.Users) .Include(ap => ap.Profiles) .Include(ap => ap.OdsInstance) .Include(ap => ap.ApplicationEducationOrganizations) .OrderBy(v => v.ApplicationName) - .Where(v => !VendorExtensions.ReservedNames.Contains(v.Vendor.VendorName.Trim())) + .Where(v => v.Vendor != null && v.Vendor.VendorName != null && !VendorExtensions.ReservedNames.Contains(v.Vendor.VendorName.Trim())) .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) .ToList(); } diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetChildResourceClaimsForParentQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetChildResourceClaimsForParentQuery.cs new file mode 100644 index 000000000..61c290cf4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetChildResourceClaimsForParentQuery.cs @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +public class GetChildResourceClaimsForParentQuery +{ + private readonly ISecurityContext _securityContext; + + public GetChildResourceClaimsForParentQuery(ISecurityContext securityContext) + => _securityContext = securityContext; + + public IEnumerable Execute(int parentResourceClaimId) + { + var parentResourceClaim = _securityContext.ResourceClaims.SingleOrDefault( + rc => rc.ResourceClaimId == parentResourceClaimId); + + var childResourcesForParent = _securityContext.ResourceClaims + .Where(x => x.ParentResourceClaimId == parentResourceClaimId).ToList(); + + return [.. childResourcesForParent + .Select(x => new ResourceClaim() + { + Name = x.ResourceName, + Id = x.ResourceClaimId, + ParentId = parentResourceClaimId, + ParentName = parentResourceClaim?.ResourceName ?? string.Empty, + }) + .Distinct() + .OrderBy(x => x.Name)]; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetOdsInstanceQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetOdsInstanceQuery.cs new file mode 100644 index 000000000..cc2c46256 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetOdsInstanceQuery.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +public interface IGetOdsInstanceQuery +{ + OdsInstance Execute(int odsInstanceId); +} + +public class GetOdsInstanceQuery : IGetOdsInstanceQuery +{ + private readonly IUsersContext _usersContext; + + public GetOdsInstanceQuery(IUsersContext userContext) + { + _usersContext = userContext; + } + + public OdsInstance Execute(int odsInstanceId) + { + return _usersContext.OdsInstances + .SingleOrDefault(odsInstance => odsInstance.OdsInstanceId == odsInstanceId) ?? throw new NotFoundException("odsInstance", odsInstanceId); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetOdsInstancesQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetOdsInstancesQuery.cs new file mode 100644 index 000000000..54036cedd --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetOdsInstancesQuery.cs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Extensions; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +public interface IGetOdsInstancesQuery +{ + List Execute(); + + List Execute(CommonQueryParams commonQueryParams); +} + +public class GetOdsInstancesQuery : IGetOdsInstancesQuery +{ + private readonly IUsersContext _usersContext; + private readonly IOptions _options; + + public GetOdsInstancesQuery(IUsersContext userContext, IOptions options) + { + _usersContext = userContext; + _options = options; + } + + public List Execute() + { + return _usersContext.OdsInstances.OrderBy(odsInstance => odsInstance.Name).ToList(); + } + + public List Execute(CommonQueryParams commonQueryParams) + { + return _usersContext.OdsInstances + .OrderBy(o => o.Name) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .ToList(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetProfilesQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetProfilesQuery.cs new file mode 100644 index 000000000..9027310ca --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetProfilesQuery.cs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +public class GetProfilesQuery +{ + private readonly IUsersContext _usersContext; + + public GetProfilesQuery(IUsersContext usersContext) + { + _usersContext = usersContext; + } + + public List Execute() + { + return _usersContext.Profiles.OrderBy(p => p.ProfileName).ToList(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetResourceClaimsAsFlatListQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetResourceClaimsAsFlatListQuery.cs new file mode 100644 index 000000000..e3091bea7 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetResourceClaimsAsFlatListQuery.cs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +public interface IGetResourceClaimsAsFlatListQuery +{ + IReadOnlyList Execute(); +} + +public class GetResourceClaimsAsFlatListQuery : IGetResourceClaimsAsFlatListQuery +{ + private readonly ISecurityContext _securityContext; + + public GetResourceClaimsAsFlatListQuery(ISecurityContext securityContext) => _securityContext = securityContext; + + public IReadOnlyList Execute() + { + return [.. _securityContext.ResourceClaims + .Select(x => new ResourceClaim + { + Id = x.ResourceClaimId, + Name = x.ResourceName, + ParentId = x.ParentResourceClaimId ?? 0, + ParentName = x.ParentResourceClaim != null ? x.ParentResourceClaim.ResourceName : null + }) + .Distinct() + .OrderBy(x => x.Name)]; + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourceClaims53Query.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetResourceClaimsQuery.cs similarity index 70% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourceClaims53Query.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetResourceClaimsQuery.cs index c4d796df5..4c904f737 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetResourceClaims53Query.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetResourceClaimsQuery.cs @@ -2,24 +2,23 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; -using System.Collections.Generic; -using System.Linq; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; -public class GetResourceClaims53Query : IGetResourceClaimsQuery +public interface IGetResourceClaimsQuery +{ + IEnumerable Execute(); +} + +public class GetResourceClaimsQuery : IGetResourceClaimsQuery { private readonly ISecurityContext _securityContext; - public GetResourceClaims53Query(ISecurityContext securityContext) - { - _securityContext = securityContext; - } + public GetResourceClaimsQuery(ISecurityContext securityContext) + => _securityContext = securityContext; public IEnumerable Execute() { @@ -42,9 +41,8 @@ public IEnumerable Execute() Id = parentResource.ResourceClaimId }); } - return resources + return [.. resources .Distinct() - .OrderBy(x => x.Name) - .ToList(); + .OrderBy(x => x.Name)]; } } diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetVendorByIdQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetVendorByIdQuery.cs new file mode 100644 index 000000000..ef998ab6c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetVendorByIdQuery.cs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +public interface IGetVendorByIdQuery +{ + Vendor? Execute(int vendorId); +} + +public class GetVendorByIdQuery : IGetVendorByIdQuery +{ + private readonly IUsersContext _context; + + public GetVendorByIdQuery(IUsersContext context) + { + _context = context; + } + + public Vendor? Execute(int vendorId) + { + return _context.Vendors + .Include(x => x.VendorNamespacePrefixes) + .Include(x => x.Users) + .SingleOrDefault(v => v.VendorId == vendorId); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetVendorsQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetVendorsQuery.cs new file mode 100644 index 000000000..6d51b4a67 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/GetVendorsQuery.cs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +public interface IGetVendorsQuery +{ + List Execute(); + List Execute(CommonQueryParams commonQueryParams); +} + +public class GetVendorsQuery : IGetVendorsQuery +{ + private readonly IUsersContext _context; + private readonly IOptions _options; + + public GetVendorsQuery(IUsersContext context, IOptions options) + { + _context = context; + _options = options; + } + + public List Execute() + { + return _context.Vendors + .Include(vn => vn.VendorNamespacePrefixes) + .Include(x => x.Users) + .Include(x => x.Applications).ThenInclude(x => x.ApplicationEducationOrganizations) + .Include(x => x.Applications).ThenInclude(x => x.Profiles) + .Include(x => x.Applications).ThenInclude(x => x.OdsInstance) + .OrderBy(v => v.VendorName).Where(v => v.VendorName != null && !VendorExtensions.ReservedNames.Contains(v.VendorName.Trim())) + .ToList(); + } + + public List Execute(CommonQueryParams commonQueryParams) + { + return _context.Vendors + .Include(vn => vn.VendorNamespacePrefixes) + .Include(x => x.Users) + .Include(x => x.Applications).ThenInclude(x => x.ApplicationEducationOrganizations) + .Include(x => x.Applications).ThenInclude(x => x.Profiles) + .Include(x => x.Applications).ThenInclude(x => x.OdsInstance) + .OrderBy(v => v.VendorName).Where(v => v.VendorName != null && !VendorExtensions.ReservedNames.Contains(v.VendorName.Trim())) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .ToList(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/VendorExtensions.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/VendorExtensions.cs new file mode 100644 index 000000000..bb0dc5a6d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Database/Queries/VendorExtensions.cs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; + +public static class VendorExtensions +{ + public static readonly string[] ReservedNames = + { + Constants.VendorName + }; + + public static bool IsSystemReservedVendorName(string? vendorName) + { + return ReservedNames.Contains(vendorName?.Trim()); + } + + public static bool IsSystemReservedVendor(this Vendor vendor) + { + return IsSystemReservedVendorName(vendor?.VendorName); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/ListExplicitSchemaDocumentFilter.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/ListExplicitSchemaDocumentFilter.cs new file mode 100644 index 000000000..15f6e6feb --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/ListExplicitSchemaDocumentFilter.cs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Features; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Documentation; + +public class ListExplicitSchemaDocumentFilter : IDocumentFilter +{ + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + context.SchemaGenerator.GenerateSchema(typeof(AdminApiResponse), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(AdminApiError), context.SchemaRepository); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/SwaggerDefaultParameterFilter.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/SwaggerDefaultParameterFilter.cs new file mode 100644 index 000000000..fde63e832 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/SwaggerDefaultParameterFilter.cs @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.V1.Features; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Documentation; + +public class SwaggerDefaultParameterFilter : IOperationFilter +{ + private readonly IOptions _settings; + + public SwaggerDefaultParameterFilter(IOptions settings) + { + _settings = settings; + } + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + foreach (var parameter in operation.Parameters) + { + if (parameter.Name.ToLower().Equals("offset")) + { + parameter.Description = "Indicates how many items should be skipped before returning results."; + parameter.Schema.Default = new OpenApiString(_settings.Value.DefaultPageSizeOffset.ToString()); + } + else if (parameter.Name.ToLower().Equals("limit")) + { + parameter.Description = "Indicates the maximum number of items that should be returned in the results."; + parameter.Schema.Default = new OpenApiString(_settings.Value.DefaultPageSizeLimit.ToString()); + } + } + + switch (context.MethodInfo.Name) + { + case "GetVendors": + { + foreach (var parameter in operation.Parameters) + { + if (parameter.Name.ToLower().Equals("id")) + { + parameter.Description = FeatureConstants.VendorIdDescription; + } + else if (parameter.Name.ToLower().Equals("company")) + { + parameter.Description = FeatureConstants.VendorNameDescription; + } + else if (parameter.Name.ToLower().Equals("namespaceprefixes")) + { + parameter.Description = FeatureConstants.VendorNamespaceDescription; + } + else if (parameter.Name.ToLower().Equals("contactname")) + { + parameter.Description = FeatureConstants.VendorContactDescription; + } + else if (parameter.Name.ToLower().Equals("contactemailaddress")) + { + parameter.Description = FeatureConstants.VendorContactEmailDescription; + } + } + break; + } + case "GetClaimSets": + { + foreach (var parameter in operation.Parameters) + { + if (parameter.Name.ToLower().Equals("id")) + { + parameter.Description = FeatureConstants.ClaimSetIdDescription; + } + else if (parameter.Name.ToLower().Equals("name")) + { + parameter.Description = FeatureConstants.ClaimSetNameDescription; + } + } + break; + } + case "GetApplications": + { + foreach (var parameter in operation.Parameters) + { + if (parameter.Name.ToLower().Equals("id")) + { + parameter.Description = FeatureConstants.ApplicationIdDescription; + } + else if (parameter.Name.ToLower().Equals("applicationname")) + { + parameter.Description = FeatureConstants.ApplicationNameDescription; + } + else if (parameter.Name.ToLower().Equals("claimsetname")) + { + parameter.Description = FeatureConstants.ClaimSetNameDescription; + } + } + break; + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/SwaggerExcludeSchemaFilter.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/SwaggerExcludeSchemaFilter.cs new file mode 100644 index 000000000..9730e9703 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/SwaggerExcludeSchemaFilter.cs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Reflection; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Documentation; diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/SwaggerOptionalSchemaFilter.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/SwaggerOptionalSchemaFilter.cs new file mode 100644 index 000000000..8f0fe3cb8 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/SwaggerOptionalSchemaFilter.cs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Reflection; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Documentation; + +[AttributeUsage(AttributeTargets.Property)] +public class SwaggerOptionalAttribute : Attribute +{ +} + +public class SwaggerOptionalSchemaFilter : ISchemaFilter +{ + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + var properties = context.Type.GetProperties(); + + foreach (var property in properties) + { + var attribute = property.GetCustomAttribute(typeof(SwaggerOptionalAttribute)); + var propertyNameInCamelCasing = char.ToLowerInvariant(property.Name[0]) + property.Name[1..]; + + if (attribute != null) + { + schema.Required?.Remove(propertyNameInCamelCasing); + } + else + { + if (schema.Required == null) + { + schema.Required = new HashSet() { propertyNameInCamelCasing }; + } + else + { + schema.Required.Add(propertyNameInCamelCasing); + } + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/TagByResourceUrlFilter.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/TagByResourceUrlFilter.cs new file mode 100644 index 000000000..da73be48e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Documentation/TagByResourceUrlFilter.cs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Text.RegularExpressions; +using EdFi.Ods.AdminApi.V1.Infrastructure.Extensions; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Documentation; + +public class TagByResourceUrlFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var urlParts = context.ApiDescription.RelativePath?.Split("/") ?? Array.Empty(); + + if (urlParts.Length == 0) + return; + + var isVersionPart = new Regex("(v)\\d+"); + var resourceName = isVersionPart.IsMatch(urlParts[0]) + ? urlParts[1] : urlParts[0]; + + if (!string.IsNullOrWhiteSpace(resourceName)) + operation.Tags = new List { new() { Name = resourceName.Trim('/').ToTitleCase() } }; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Extensions/AdminModelExtensions.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Extensions/AdminModelExtensions.cs new file mode 100644 index 000000000..c2587cc2f --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Extensions/AdminModelExtensions.cs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; +using Profile = EdFi.Ods.AdminApi.V1.Features.Applications.Profile; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure; + +public static class AdminModelExtensions +{ + public static string? ContactName(this Vendor vendor) + { + return vendor?.Users?.FirstOrDefault()?.FullName; + } + + public static string? ContactEmail(this Vendor vendor) + { + return vendor?.Users?.FirstOrDefault()?.Email; + } + + public static string? ProfileName(this Application application) + { + return application?.Profiles?.FirstOrDefault()?.ProfileName; + } + public static string? OdsInstanceName(this Application application) + { + return application?.OdsInstance?.Name; + } + + public static IList Profiles(this Application application) + { + var profiles = new List(); + foreach (var profile in application.Profiles) + { + profiles.Add(new Profile { Id = profile.ProfileId }); + } + return profiles; + } + + public static int? VendorId(this Application application) + { + return application?.Vendor?.VendorId; + } + + public static IList? EducationOrganizationIds(this Application application) + { + return application?.ApplicationEducationOrganizations?.Select(eu => eu.EducationOrganizationId).ToList(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Extensions/QueryExtensions.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Extensions/QueryExtensions.cs new file mode 100644 index 000000000..d0408301d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Extensions/QueryExtensions.cs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Settings; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Extensions; +public static class QueryExtensions +{ + /// + /// Apply pagination based on the offset and limit + /// + /// Type of entity + /// IQueryable entity list to apply the pagination + /// + /// + /// App Setting values + /// Paginated list + public static IQueryable Paginate(this IQueryable source, int? offset, int? limit, IOptions settings) + { + try + { + if (offset == null) + offset = settings.Value.DefaultPageSizeOffset; + + if (limit == null) + limit = settings.Value.DefaultPageSizeLimit; + + return source.Skip(offset.Value).Take(limit.Value); + } + catch (Exception) + { + // If this throws an exception simply don't paginate. + return source; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Extensions/StringExtensions.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Extensions/StringExtensions.cs similarity index 92% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Extensions/StringExtensions.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Extensions/StringExtensions.cs index a79c69e4a..28379927c 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Extensions/StringExtensions.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Extensions/StringExtensions.cs @@ -1,6 +1,6 @@ using System.Globalization; -namespace EdFi.Ods.AdminApi.Infrastructure.Extensions; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Extensions; public static class StringExtensions { diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/AdminApiV1FeatureHelper.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/AdminApiV1FeatureHelper.cs new file mode 100644 index 000000000..ab770f386 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/AdminApiV1FeatureHelper.cs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Reflection; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Helpers; + +public static class AdminApiV1FeatureHelper +{ + public static List GetFeatures() => FeatureHelper.GetFeatures(Assembly.GetExecutingAssembly()); +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/ConstantsHelper.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/ConstantsHelper.cs new file mode 100644 index 000000000..3d61eb068 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/ConstantsHelper.cs @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Reflection; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Helpers; + +public static class ConstantsHelpers +{ + /// + /// Semantic version of the admin api. + /// + public const string Version = "1.1"; + + /// + /// Assembly version of the admin api. + /// + public static readonly string Build = Assembly.GetExecutingAssembly() + .GetName() + .Version?.ToString() ?? Version; +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/FeaturesHelper.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/FeaturesHelper.cs similarity index 89% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/FeaturesHelper.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/FeaturesHelper.cs index 0a21137bb..101888bb8 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/FeaturesHelper.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/FeaturesHelper.cs @@ -3,10 +3,10 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Ods.AdminApi.Features; using System.Reflection; +using EdFi.Ods.AdminApi.Common.Features; -namespace EdFi.Ods.AdminApi.Infrastructure.Helpers; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Helpers; public static class FeaturesHelper { diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/HealthCheckServiceExtensions.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/HealthCheckServiceExtensions.cs new file mode 100644 index 000000000..ba81eefff --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/HealthCheckServiceExtensions.cs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.Extensions.DependencyInjection; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure; + +public static class HealthCheckServiceExtensions +{ + public static IServiceCollection AddHealthCheck(this IServiceCollection services, string connectionString, bool isSqlServer) + { + var hcBuilder = services.AddHealthChecks(); + if (isSqlServer) + { + hcBuilder.AddSqlServer(connectionString); + } + else + { + hcBuilder.AddNpgSql(connectionString); + } + + return services; + } + + public static IServiceCollection AddHealthCheck( + this IServiceCollection services, + string adminConnectionString, + string securityConnectionString, + bool isSqlServer) + { + var hcBuilder = services.AddHealthChecks(); + + if (isSqlServer) + { + hcBuilder.AddSqlServer(adminConnectionString, name: "EdFi_Admin", tags: ["Databases"]); + hcBuilder.AddSqlServer(securityConnectionString, name: "EdFi_Security", tags: ["Databases"]); + } + else + { + hcBuilder.AddNpgSql(adminConnectionString, name: "EdFi_Admin", tags: ["Databases"]); + hcBuilder.AddNpgSql(securityConnectionString, name: "EdFi_Security", tags: ["Databases"]); + } + + return services; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/IEnumerableExtensions.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/IEnumerableExtensions.cs new file mode 100644 index 000000000..66c07c154 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/IEnumerableExtensions.cs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + + +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Models; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Helpers; + +public static class IEnumerableExtensions +{ + public static string ToCommaSeparated(this IEnumerable vendorNamespacePrefixes) + { + return vendorNamespacePrefixes != null && vendorNamespacePrefixes.Any() + ? ToDelimiterSeparated(vendorNamespacePrefixes.Select(x => x.NamespacePrefix)) + : string.Empty; + } + + public static string ToDelimiterSeparated(this IEnumerable inputStrings, string separator = ",") + { + var listOfStrings = inputStrings.ToList(); + + return listOfStrings.Any() + ? string.Join(separator, listOfStrings) + : string.Empty; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/OdsInstanceIdentityHelper.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/OdsInstanceIdentityHelper.cs new file mode 100644 index 000000000..103a7f323 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/OdsInstanceIdentityHelper.cs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Helpers; + +public static class OdsInstanceIdentityHelper +{ + public static int GetIdentityValue(string odsInstanceName) + { + var index = odsInstanceName.LastIndexOf("_", StringComparison.InvariantCulture); + + var identityValue = odsInstanceName[(index + 1)..]; + + return int.Parse(identityValue); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/IMarkerForEdFiOdsAdminAppManagement.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/IMarkerForEdFiOdsAdminAppManagement.cs new file mode 100644 index 000000000..8cb7d078e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/IMarkerForEdFiOdsAdminAppManagement.cs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure; + +public interface IMarkerForEdFiOdsAdminApiManagement +{ +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/JsonContractResolvers/ShouldSerializeContractResolver.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/JsonContractResolvers/ShouldSerializeContractResolver.cs new file mode 100644 index 000000000..0e06f2b50 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/JsonContractResolvers/ShouldSerializeContractResolver.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Features.ClaimSets; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System.Reflection; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.JsonContractResolvers; + +public class ShouldSerializeContractResolver : DefaultContractResolver +{ + public ShouldSerializeContractResolver() : base() + { + } + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + + if (property.DeclaringType == typeof(ResourceClaimModel) && + string.Equals(property.PropertyName, "readChanges", StringComparison.OrdinalIgnoreCase)) + { + property.ShouldSerialize = _ => true; + } + + property.PropertyName = char.ToLowerInvariant(property.PropertyName![0]) + property.PropertyName[1..]; + return property; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/Action.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/Action.cs new file mode 100644 index 000000000..1f0b5347e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/Action.cs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public class Action : Enumeration +{ + public static readonly Action Create = new("Create", "Create"); + public static readonly Action Read = new("Read", "Read"); + public static readonly Action Update = new("Update", "Update"); + public static readonly Action Delete = new("Delete", "Delete"); + public static readonly Action ReadChanges = new("ReadChanges", "ReadChanges"); + + private Action(string value, string displayName) : base(value, displayName) { } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommand.cs new file mode 100644 index 000000000..c5988eb7e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommand.cs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public class AddClaimSetCommand +{ + private readonly AddClaimSetCommandService _service; + + public AddClaimSetCommand(AddClaimSetCommandService service) + { + _service = service; + } + + public int Execute(IAddClaimSetModel claimSet) + { + return _service.Execute(claimSet); + } +} + +public interface IAddClaimSetModel +{ + string ClaimSetName { get; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommandV6Service.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommandService.cs similarity index 67% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommandV6Service.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommandService.cs index 4c76a7db3..36bd934dd 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommandV6Service.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommandService.cs @@ -3,22 +3,23 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Security.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; -public class AddClaimSetCommandV6Service +public class AddClaimSetCommandService { private readonly ISecurityContext _context; - public AddClaimSetCommandV6Service(ISecurityContext context) + public AddClaimSetCommandService(ISecurityContext context) { _context = context; } public int Execute(IAddClaimSetModel claimSet) { - var newClaimSet = new EdFi.Security.DataAccess.Models.ClaimSet + var newClaimSet = new Security.DataAccess.Models.ClaimSet { ClaimSetName = claimSet.ClaimSetName, Application = _context.Applications.Single(), diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AddOrEditResourcesOnClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AddOrEditResourcesOnClaimSetCommand.cs new file mode 100644 index 000000000..d31d2a65f --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AddOrEditResourcesOnClaimSetCommand.cs @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor.Extensions; +using EdFi.Ods.AdminApi.V1.Infrastructure.Database.Queries; +using FluentValidation; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public class AddOrEditResourcesOnClaimSetCommand +{ + private readonly EditResourceOnClaimSetCommand _editResourceOnClaimSetCommand; + private readonly IGetResourceClaimsQuery _getResourceClaimsQuery; + private readonly OverrideDefaultAuthorizationStrategyCommand _overrideDefaultAuthorizationStrategyCommand; + + public AddOrEditResourcesOnClaimSetCommand(EditResourceOnClaimSetCommand editResourceOnClaimSetCommand, + IGetResourceClaimsQuery getResourceClaimsQuery, + OverrideDefaultAuthorizationStrategyCommand overrideDefaultAuthorizationStrategyCommand) + { + _editResourceOnClaimSetCommand = editResourceOnClaimSetCommand; + _getResourceClaimsQuery = getResourceClaimsQuery; + _overrideDefaultAuthorizationStrategyCommand = overrideDefaultAuthorizationStrategyCommand; + } + + public void Execute(int claimSetId, List resources) + { + + var allResources = GetDbResources(); + + var childResources = new List(); + foreach (var resourceClaims in resources.Select(x => x.Children)) + childResources.AddRange(resourceClaims); + resources.AddRange(childResources); + var currentResources = resources.Select(r => + { + var resource = allResources.FirstOrDefault(dr => (dr.Name ?? string.Empty).Equals(r.Name, StringComparison.Ordinal)); + if (resource != null) + { + resource.Create = r.Create; + resource.Read = r.Read; + resource.Update = r.Update; + resource.Delete = r.Delete; + resource.ReadChanges = r.ReadChanges; + resource.AuthStrategyOverridesForCRUD = r.AuthStrategyOverridesForCRUD; + } + return resource; + }).ToList(); + + currentResources.RemoveAll(x => x is null); + + foreach (var resource in currentResources.Where(x => x is not null)) + { + var editResourceModel = new EditResourceOnClaimSetModel + { + ClaimSetId = claimSetId, + ResourceClaim = resource + }; + + _editResourceOnClaimSetCommand.Execute(editResourceModel); + + if (resource!.AuthStrategyOverridesForCRUD != null && resource.AuthStrategyOverridesForCRUD.Any()) + { + var overrideAuthStrategyModel = new OverrideAuthorizationStrategyModel + { + ClaimSetId = claimSetId, + ResourceClaimId = resource.Id, + AuthorizationStrategyForCreate = AuthStrategyOverrideForAction(resource.AuthStrategyOverridesForCRUD.Create()), + AuthorizationStrategyForRead = AuthStrategyOverrideForAction(resource.AuthStrategyOverridesForCRUD.Read()), + AuthorizationStrategyForUpdate = AuthStrategyOverrideForAction(resource.AuthStrategyOverridesForCRUD.Update()), + AuthorizationStrategyForDelete = AuthStrategyOverrideForAction(resource.AuthStrategyOverridesForCRUD.Delete()), + AuthorizationStrategyForReadChanges = AuthStrategyOverrideForAction(resource.AuthStrategyOverridesForCRUD.ReadChanges()) + }; + _overrideDefaultAuthorizationStrategyCommand.Execute(overrideAuthStrategyModel); + } + } + + static int[] AuthStrategyOverrideForAction(ClaimSetResourceClaimActionAuthStrategies? claimSetResourceClaimActionAuthStrategies) + { + if (claimSetResourceClaimActionAuthStrategies != null && claimSetResourceClaimActionAuthStrategies.AuthorizationStrategies != null) + { + return claimSetResourceClaimActionAuthStrategies.AuthorizationStrategies.Where(p => p is not null).Select(p => p!.AuthStrategyId).ToArray(); + } + return Array.Empty(); + } + } + + private List GetDbResources() + { + var allResources = new List(); + var parentResources = _getResourceClaimsQuery.Execute().ToList(); + allResources.AddRange(parentResources); + foreach (var children in parentResources.Select(x => x.Children)) + { + allResources.AddRange(children); + } + + return allResources; + } +} + +public class AddClaimSetModel : IAddClaimSetModel +{ + public string ClaimSetName { get; set; } = string.Empty; +} + +public class EditResourceOnClaimSetModel : IEditResourceOnClaimSetModel +{ + public int ClaimSetId { get; set; } + public ResourceClaim? ResourceClaim { get; set; } +} + +public class OverrideAuthorizationStrategyModel : IOverrideDefaultAuthorizationStrategyModel +{ + public int ClaimSetId { get; set; } + public int ResourceClaimId { get; set; } + public int[]? AuthorizationStrategyForCreate { get; set; } + public int[]? AuthorizationStrategyForRead { get; set; } + public int[]? AuthorizationStrategyForUpdate { get; set; } + public int[]? AuthorizationStrategyForDelete { get; set; } + public int[]? AuthorizationStrategyForReadChanges { get; set; } + +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/Application.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/Application.cs new file mode 100644 index 000000000..7bd04aaad --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/Application.cs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public class Application +{ + public string? Name { get; set; } + public string? VendorName { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AuthStrategyResolver.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AuthStrategyResolver.cs new file mode 100644 index 000000000..54d7bca1d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AuthStrategyResolver.cs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public interface IAuthStrategyResolver +{ + IEnumerable ResolveAuthStrategies(IEnumerable resourceClaims); +} + +public class AuthStrategyResolver : IAuthStrategyResolver +{ + private readonly ISecurityContext _securityContext; + + public AuthStrategyResolver(ISecurityContext securityContext) + { + _securityContext = securityContext; + } + + public IEnumerable ResolveAuthStrategies(IEnumerable resourceClaims) + { + var dbAuthStrategies = _securityContext.AuthorizationStrategies; + + foreach (var claim in resourceClaims) + { + if (claim.DefaultAuthStrategiesForCRUD != null && claim.DefaultAuthStrategiesForCRUD.Any()) + { + foreach (var claimSetResourceClaimActionAuthStrategyItem in claim.DefaultAuthStrategiesForCRUD.Where(x => x != null)) + { + if (claimSetResourceClaimActionAuthStrategyItem != null && claimSetResourceClaimActionAuthStrategyItem.AuthorizationStrategies != null) + { + foreach (var authorizationStrategyItem in claimSetResourceClaimActionAuthStrategyItem.AuthorizationStrategies) + { + if (authorizationStrategyItem is null) continue; + + var authStrategy = dbAuthStrategies.AsEnumerable().SingleOrDefault( + x => x.AuthorizationStrategyName.Equals( + authorizationStrategyItem.AuthStrategyName, StringComparison.InvariantCultureIgnoreCase)); + + if (authStrategy != null) + { + authorizationStrategyItem.AuthStrategyId = authStrategy.AuthorizationStrategyId; + authorizationStrategyItem.DisplayName = authStrategy.DisplayName; + } + } + } + + + } + } + + if (claim.AuthStrategyOverridesForCRUD != null && claim.AuthStrategyOverridesForCRUD.Any()) + { + foreach (var authStrategyOverride in claim.AuthStrategyOverridesForCRUD.Where(x => x != null)) + { + if (authStrategyOverride != null && authStrategyOverride.AuthorizationStrategies != null) + { + foreach (var authorizationStrategyItem in authStrategyOverride.AuthorizationStrategies) + { + if (authorizationStrategyItem is null) continue; + + var authStrategy = dbAuthStrategies.AsEnumerable().SingleOrDefault( + x => x.AuthorizationStrategyName.Equals( + authorizationStrategyItem.AuthStrategyName, + StringComparison.InvariantCultureIgnoreCase)); + + if (authStrategy != null) + { + authorizationStrategyItem.AuthStrategyId = authStrategy.AuthorizationStrategyId; + authorizationStrategyItem.DisplayName = authStrategy.DisplayName; + } + } + } + } + } + + if (claim.Children?.Any() ?? false) + { + claim.Children = ResolveAuthStrategies(claim.Children).ToList(); + } + + yield return claim; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AuthorizationStrategy.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AuthorizationStrategy.cs new file mode 100644 index 000000000..b3c2dd9d4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/AuthorizationStrategy.cs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public class AuthorizationStrategy +{ + public int AuthStrategyId { get; set; } + public string? AuthStrategyName { get; set; } + public string? DisplayName { get; set; } + public bool IsInheritedFromParent { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/ClaimSet.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/ClaimSet.cs new file mode 100644 index 000000000..0ce34145f --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/ClaimSet.cs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor +{ + public class ClaimSet + { + public int Id { get; set; } + public string? Name { get; set; } + public bool IsEditable { get; set; } + public int ApplicationsCount { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/ClaimSetResourceClaimActionAuthStrategies.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/ClaimSetResourceClaimActionAuthStrategies.cs new file mode 100644 index 000000000..6f230c886 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/ClaimSetResourceClaimActionAuthStrategies.cs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public interface IClaimSetResourceClaimActionAuthStrategies +{ + IList AuthorizationStrategies { get; } +} + +[SwaggerSchema(Title = "ClaimSetResourceClaimActionAuthorizationStrategies")] +public class ClaimSetResourceClaimActionAuthStrategies : IClaimSetResourceClaimActionAuthStrategies +{ + public IList AuthorizationStrategies { get; set; } = new List(); +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommand.cs new file mode 100644 index 000000000..6c1d93a9d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommand.cs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public interface IDeleteClaimSetCommand +{ + void Execute(IDeleteClaimSetModel claimSet); +} + +public class DeleteClaimSetCommand : IDeleteClaimSetCommand +{ + private readonly DeleteClaimSetCommandService _service; + + public DeleteClaimSetCommand(DeleteClaimSetCommandService service) + { + _service = service; + } + + public void Execute(IDeleteClaimSetModel claimSet) + { + _service.Execute(claimSet); + } +} + +public interface IDeleteClaimSetModel +{ + string? Name { get; } + int Id { get; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommandV6Service.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommandService.cs similarity index 82% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommandV6Service.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommandService.cs index e121d8356..cb561f8c7 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommandV6Service.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommandService.cs @@ -3,17 +3,16 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System; -using System.Linq; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using EdFi.Security.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -public class DeleteClaimSetCommandV6Service +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +public class DeleteClaimSetCommandService { private readonly ISecurityContext _context; - public DeleteClaimSetCommandV6Service(ISecurityContext context) + public DeleteClaimSetCommandService(ISecurityContext context) { _context = context; } diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommand.cs new file mode 100644 index 000000000..886b3d47e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommand.cs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor +{ + public interface IEditClaimSetCommand + { + int Execute(IEditClaimSetModel claimSet); + } + + public class EditClaimSetCommand : IEditClaimSetCommand + { + private readonly EditClaimSetCommandService _service; + + public EditClaimSetCommand(EditClaimSetCommandService service) + { + _service = service; + } + + public int Execute(IEditClaimSetModel claimSet) + { + return _service.Execute(claimSet); + } + } + + public interface IEditClaimSetModel + { + string? ClaimSetName { get; } + int ClaimSetId { get; } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommandV6Service.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommandService.cs similarity index 81% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommandV6Service.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommandService.cs index ef81dd8f6..ee1cc7d10 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommandV6Service.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommandService.cs @@ -3,18 +3,19 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Security.DataAccess.Contexts; -using EdFi.Admin.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; -public class EditClaimSetCommandV6Service +public class EditClaimSetCommandService { private readonly ISecurityContext _securityContext; private readonly IUsersContext _usersContext; - public EditClaimSetCommandV6Service(ISecurityContext securityContext, IUsersContext usersContext) + public EditClaimSetCommandService(ISecurityContext securityContext, IUsersContext usersContext) { _securityContext = securityContext; _usersContext = usersContext; diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommand.cs new file mode 100644 index 000000000..ecfa25145 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommand.cs @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public class EditResourceOnClaimSetCommand(EditResourceOnClaimSetCommandService service) +{ + private readonly EditResourceOnClaimSetCommandService _service = service; + + public void Execute(IEditResourceOnClaimSetModel model) + { + _service.Execute(model); + } +} + +public interface IEditResourceOnClaimSetModel +{ + int ClaimSetId { get; } + ResourceClaim? ResourceClaim { get; } +} + diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommandV6Service.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommandService.cs similarity index 93% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommandV6Service.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommandService.cs index f2e75f032..1bbee4c48 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommandV6Service.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommandService.cs @@ -3,17 +3,17 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Security.DataAccess.Contexts; -using EdFi.Security.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; using Microsoft.EntityFrameworkCore; -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; -public class EditResourceOnClaimSetCommandV6Service +public class EditResourceOnClaimSetCommandService { private readonly ISecurityContext _context; - public EditResourceOnClaimSetCommandV6Service(ISecurityContext context) + public EditResourceOnClaimSetCommandService(ISecurityContext context) { _context = context; } @@ -60,7 +60,7 @@ private void RemoveDisabledActionsFromClaimSet(ResourceClaim modelResourceClaim, else if (claimSetResourceClaim.Action.ActionName == Action.Delete.Value && !modelResourceClaim.Delete) { recordsToRemove.Add(claimSetResourceClaim); - } + } else if (claimSetResourceClaim.Action.ActionName == Action.ReadChanges.Value && !modelResourceClaim.ReadChanges) { recordsToRemove.Add(claimSetResourceClaim); @@ -73,7 +73,7 @@ private void RemoveDisabledActionsFromClaimSet(ResourceClaim modelResourceClaim, } } - private void AddEnabledActionsToClaimSet(ResourceClaim modelResourceClaim, IReadOnlyCollection claimSetResourceClaimsToEdit, EdFi.Security.DataAccess.Models.ClaimSet claimSetToEdit) + private void AddEnabledActionsToClaimSet(ResourceClaim modelResourceClaim, IReadOnlyCollection claimSetResourceClaimsToEdit, Security.DataAccess.Models.ClaimSet claimSetToEdit) { var actionsFromDb = _context.Actions.ToList(); @@ -119,8 +119,8 @@ private void AddEnabledActionsToClaimSet(ResourceClaim modelResourceClaim, IRead ClaimSet = claimSetToEdit, ResourceClaim = resourceClaimFromDb }); - } - + } + if (modelResourceClaim.ReadChanges && claimSetResourceClaimsToEdit.All(x => x.Action.ActionName != Action.ReadChanges.Value)) { recordsToAdd.Add(new ClaimSetResourceClaimAction diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/Extensions/AuthorizationStrategiesExtension.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/Extensions/AuthorizationStrategiesExtension.cs new file mode 100644 index 000000000..0fbf1e179 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/Extensions/AuthorizationStrategiesExtension.cs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor.Extensions; + +public static class AuthorizationStrategiesExtension +{ + public static AuthorizationStrategy? Create(this AuthorizationStrategy?[] authorizationStrategies) + { + return authorizationStrategies.Length > 0 ? authorizationStrategies[0] : null; + } + + public static AuthorizationStrategy? Read(this AuthorizationStrategy?[] authorizationStrategies) + { + return authorizationStrategies.Length > 1 ? authorizationStrategies[1] : null; + } + + public static AuthorizationStrategy? Update(this AuthorizationStrategy?[] authorizationStrategies) + { + return authorizationStrategies.Length > 2 ? authorizationStrategies[2] : null; + } + + public static AuthorizationStrategy? Delete(this AuthorizationStrategy?[] authorizationStrategies) + { + return authorizationStrategies.Length > 3 ? authorizationStrategies[3] : null; + } + + public static AuthorizationStrategy? ReadChanges(this AuthorizationStrategy?[] authorizationStrategies) + { + return authorizationStrategies.Length > 4 ? authorizationStrategies[4] : null; + } + + public static AuthorizationStrategy?[] AddAuthorizationStrategyOverrides(this AuthorizationStrategy?[] authorizationStrategies, + string actionName, AuthorizationStrategy? strategy) + { + if (actionName == Action.Create.Value) + authorizationStrategies[0] = strategy; + else if (actionName == Action.Read.Value) + authorizationStrategies[1] = strategy; + else if (actionName == Action.Update.Value) + authorizationStrategies[2] = strategy; + else if (actionName == Action.Delete.Value) + authorizationStrategies[3] = strategy; + else if (actionName == Action.ReadChanges.Value) + authorizationStrategies[4] = strategy; + + return authorizationStrategies; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/Extensions/ClaimSetResourceClaimActionAuthStrategiesExtension.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/Extensions/ClaimSetResourceClaimActionAuthStrategiesExtension.cs similarity index 90% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/Extensions/ClaimSetResourceClaimActionAuthStrategiesExtension.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/Extensions/ClaimSetResourceClaimActionAuthStrategiesExtension.cs index fdd2b2992..4e36411c7 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/Extensions/ClaimSetResourceClaimActionAuthStrategiesExtension.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/Extensions/ClaimSetResourceClaimActionAuthStrategiesExtension.cs @@ -1,77 +1,74 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.Extensions; -using Action = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.Action; - -namespace EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor.Extensions; - -public static class ClaimSetResourceClaimActionAuthStrategiesExtension -{ - public static ClaimSetResourceClaimActionAuthStrategies? Create(this ClaimSetResourceClaimActionAuthStrategies?[] claimSetResourceClaimActionAuthStrategies) - { - return claimSetResourceClaimActionAuthStrategies.Length > 0 ? claimSetResourceClaimActionAuthStrategies[0] : null; - } - - public static ClaimSetResourceClaimActionAuthStrategies? Read(this ClaimSetResourceClaimActionAuthStrategies?[] claimSetResourceClaimActionAuthStrategies) - { - return claimSetResourceClaimActionAuthStrategies.Length > 1 ? claimSetResourceClaimActionAuthStrategies[1] : null; - } - - public static ClaimSetResourceClaimActionAuthStrategies? Update(this ClaimSetResourceClaimActionAuthStrategies?[] claimSetResourceClaimActionAuthStrategies) - { - return claimSetResourceClaimActionAuthStrategies.Length > 2 ? claimSetResourceClaimActionAuthStrategies[2] : null; - } - - public static ClaimSetResourceClaimActionAuthStrategies? Delete(this ClaimSetResourceClaimActionAuthStrategies?[] claimSetResourceClaimActionAuthStrategies) - { - return claimSetResourceClaimActionAuthStrategies.Length > 3 ? claimSetResourceClaimActionAuthStrategies[3] : null; - } - - public static ClaimSetResourceClaimActionAuthStrategies? ReadChanges(this ClaimSetResourceClaimActionAuthStrategies?[] claimSetResourceClaimActionAuthStrategies) - { - return claimSetResourceClaimActionAuthStrategies.Length > 4 ? claimSetResourceClaimActionAuthStrategies[4] : null; - } - - public static ClaimSetResourceClaimActionAuthStrategies?[] AddAuthorizationStrategyOverrides(this ClaimSetResourceClaimActionAuthStrategies?[] claimSetResourceClaimActionAuthStrategies, - string actionName, AuthorizationStrategy? strategy) - { - if (strategy == null) return claimSetResourceClaimActionAuthStrategies; - - if (actionName == Action.Create.Value) - { - if (claimSetResourceClaimActionAuthStrategies[0] == null) - claimSetResourceClaimActionAuthStrategies[0] = new ClaimSetResourceClaimActionAuthStrategies(); - claimSetResourceClaimActionAuthStrategies[0]!.AuthorizationStrategies.Add(strategy); - } - else if (actionName == Action.Read.Value) - { - if (claimSetResourceClaimActionAuthStrategies[1] == null) - claimSetResourceClaimActionAuthStrategies[1] = new ClaimSetResourceClaimActionAuthStrategies(); - claimSetResourceClaimActionAuthStrategies[1]!.AuthorizationStrategies.Add(strategy); - } - else if (actionName == Action.Update.Value) - { - if (claimSetResourceClaimActionAuthStrategies[2] == null) - claimSetResourceClaimActionAuthStrategies[2] = new ClaimSetResourceClaimActionAuthStrategies(); - claimSetResourceClaimActionAuthStrategies[2]!.AuthorizationStrategies.Add(strategy); - } - else if (actionName == Action.Delete.Value) - { - if (claimSetResourceClaimActionAuthStrategies[3] == null) - claimSetResourceClaimActionAuthStrategies[3] = new ClaimSetResourceClaimActionAuthStrategies(); - claimSetResourceClaimActionAuthStrategies[3]!.AuthorizationStrategies.Add(strategy); - } - else if (actionName == Action.ReadChanges.Value && claimSetResourceClaimActionAuthStrategies.Length == 5) - { - if (claimSetResourceClaimActionAuthStrategies[4] == null) - claimSetResourceClaimActionAuthStrategies[4] = new ClaimSetResourceClaimActionAuthStrategies(); - claimSetResourceClaimActionAuthStrategies[4]!.AuthorizationStrategies.Add(strategy); - } - - return claimSetResourceClaimActionAuthStrategies; - } -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor.Extensions; + +public static class ClaimSetResourceClaimActionAuthStrategiesExtension +{ + public static ClaimSetResourceClaimActionAuthStrategies? Create(this ClaimSetResourceClaimActionAuthStrategies?[] claimSetResourceClaimActionAuthStrategies) + { + return claimSetResourceClaimActionAuthStrategies.Length > 0 ? claimSetResourceClaimActionAuthStrategies[0] : null; + } + + public static ClaimSetResourceClaimActionAuthStrategies? Read(this ClaimSetResourceClaimActionAuthStrategies?[] claimSetResourceClaimActionAuthStrategies) + { + return claimSetResourceClaimActionAuthStrategies.Length > 1 ? claimSetResourceClaimActionAuthStrategies[1] : null; + } + + public static ClaimSetResourceClaimActionAuthStrategies? Update(this ClaimSetResourceClaimActionAuthStrategies?[] claimSetResourceClaimActionAuthStrategies) + { + return claimSetResourceClaimActionAuthStrategies.Length > 2 ? claimSetResourceClaimActionAuthStrategies[2] : null; + } + + public static ClaimSetResourceClaimActionAuthStrategies? Delete(this ClaimSetResourceClaimActionAuthStrategies?[] claimSetResourceClaimActionAuthStrategies) + { + return claimSetResourceClaimActionAuthStrategies.Length > 3 ? claimSetResourceClaimActionAuthStrategies[3] : null; + } + + public static ClaimSetResourceClaimActionAuthStrategies? ReadChanges(this ClaimSetResourceClaimActionAuthStrategies?[] claimSetResourceClaimActionAuthStrategies) + { + return claimSetResourceClaimActionAuthStrategies.Length > 4 ? claimSetResourceClaimActionAuthStrategies[4] : null; + } + + public static ClaimSetResourceClaimActionAuthStrategies?[] AddAuthorizationStrategyOverrides(this ClaimSetResourceClaimActionAuthStrategies?[] claimSetResourceClaimActionAuthStrategies, + string actionName, AuthorizationStrategy? strategy) + { + if (strategy == null) + return claimSetResourceClaimActionAuthStrategies; + + if (actionName == Action.Create.Value) + { + if (claimSetResourceClaimActionAuthStrategies[0] == null) + claimSetResourceClaimActionAuthStrategies[0] = new ClaimSetResourceClaimActionAuthStrategies(); + claimSetResourceClaimActionAuthStrategies[0]!.AuthorizationStrategies.Add(strategy); + } + else if (actionName == Action.Read.Value) + { + if (claimSetResourceClaimActionAuthStrategies[1] == null) + claimSetResourceClaimActionAuthStrategies[1] = new ClaimSetResourceClaimActionAuthStrategies(); + claimSetResourceClaimActionAuthStrategies[1]!.AuthorizationStrategies.Add(strategy); + } + else if (actionName == Action.Update.Value) + { + if (claimSetResourceClaimActionAuthStrategies[2] == null) + claimSetResourceClaimActionAuthStrategies[2] = new ClaimSetResourceClaimActionAuthStrategies(); + claimSetResourceClaimActionAuthStrategies[2]!.AuthorizationStrategies.Add(strategy); + } + else if (actionName == Action.Delete.Value) + { + if (claimSetResourceClaimActionAuthStrategies[3] == null) + claimSetResourceClaimActionAuthStrategies[3] = new ClaimSetResourceClaimActionAuthStrategies(); + claimSetResourceClaimActionAuthStrategies[3]!.AuthorizationStrategies.Add(strategy); + } + else if (actionName == Action.ReadChanges.Value && claimSetResourceClaimActionAuthStrategies.Length == 5) + { + if (claimSetResourceClaimActionAuthStrategies[4] == null) + claimSetResourceClaimActionAuthStrategies[4] = new ClaimSetResourceClaimActionAuthStrategies(); + claimSetResourceClaimActionAuthStrategies[4]!.AuthorizationStrategies.Add(strategy); + } + + return claimSetResourceClaimActionAuthStrategies; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetAllAuthorizationStrategiesQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetAllAuthorizationStrategiesQuery.cs new file mode 100644 index 000000000..158812062 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetAllAuthorizationStrategiesQuery.cs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +public interface IGetAllAuthorizationStrategiesQuery +{ + IReadOnlyList Execute(); +} + +public class GetAllAuthorizationStrategiesQuery : IGetAllAuthorizationStrategiesQuery +{ + private readonly ISecurityContext _securityContext; + + public GetAllAuthorizationStrategiesQuery(ISecurityContext securityContext) + { + _securityContext = securityContext; + } + + public IReadOnlyList Execute() + { + return _securityContext.AuthorizationStrategies + .OrderBy(x => x.AuthorizationStrategyName) + .Select(x => new AuthorizationStrategy + { + AuthStrategyId = x.AuthorizationStrategyId, + AuthStrategyName = x.AuthorizationStrategyName, + DisplayName = x.DisplayName + }).ToList(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQuery.cs new file mode 100644 index 000000000..dac9c4fe3 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQuery.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Infrastructure; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public interface IGetAllClaimSetsQuery +{ + IReadOnlyList Execute(); + IReadOnlyList Execute(CommonQueryParams commonQueryParams); +} + +public class GetAllClaimSetsQuery : IGetAllClaimSetsQuery +{ + private readonly GetAllClaimSetsQueryService _service; + + public GetAllClaimSetsQuery(GetAllClaimSetsQueryService service) => _service = service; + + public IReadOnlyList Execute() + { + return _service.Execute(); + } + + public IReadOnlyList Execute(CommonQueryParams commonQueryParams) + { + return _service.Execute(commonQueryParams); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQueryV6Service.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQueryService.cs similarity index 62% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQueryV6Service.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQueryService.cs index 356919f06..d9fb12f00 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQueryV6Service.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQueryService.cs @@ -3,29 +3,23 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Ods.AdminApi.Helpers; -using EdFi.Ods.AdminApi.Infrastructure.Extensions; -using EdFi.Security.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.V1.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; using Microsoft.Extensions.Options; -using ClaimSet = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ClaimSet; -namespace EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; -public class GetAllClaimSetsQueryV6Service +public class GetAllClaimSetsQueryService(ISecurityContext securityContext, IOptions options) { - private readonly ISecurityContext _securityContext; - private readonly IOptions _options; - - public GetAllClaimSetsQueryV6Service(ISecurityContext securityContext, IOptions options) - { - _securityContext = securityContext; - _options = options; - } + private readonly ISecurityContext _securityContext = securityContext; + private readonly IOptions _options = options; public IReadOnlyList Execute() { - return _securityContext.ClaimSets + return [.. _securityContext.ClaimSets .Select(x => new ClaimSet { Id = x.ClaimSetId, @@ -34,13 +28,12 @@ public IReadOnlyList Execute() !Constants.SystemReservedClaimSets.Contains(x.ClaimSetName) }) .Distinct() - .OrderBy(x => x.Name) - .ToList(); + .OrderBy(x => x.Name)]; } public IReadOnlyList Execute(CommonQueryParams commonQueryParams) { - return _securityContext.ClaimSets + return [.. _securityContext.ClaimSets .Select(x => new ClaimSet { Id = x.ClaimSetId, @@ -50,7 +43,6 @@ public IReadOnlyList Execute(CommonQueryParams commonQueryParams) }) .Distinct() .OrderBy(x => x.Name) - .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) - .ToList(); + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options)]; } } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetId53Query.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetApplicationsByClaimSetIdQuery.cs similarity index 53% rename from Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetId53Query.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetApplicationsByClaimSetIdQuery.cs index 513a569c6..fa2560dc7 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/ClaimSetEditorTests/GetApplicationsByClaimSetId53Query.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetApplicationsByClaimSetIdQuery.cs @@ -2,37 +2,27 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; -using System.Collections.Generic; -using System.Linq; -using EdFi.Admin.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; -namespace EdFi.Ods.AdminApi.DBTests.ClaimSetEditorTests; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; -/// -/// Compatibility copy of EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.GetApplicationsByClaimSetIdQuery -/// -/// Since the projected ClaimSet does not include the new columns from v6.1, this query does not -/// require multiple versions. However, in order to preserve consistent test results, we must -/// construct dependent services using a query against the same database. -/// -internal class GetApplicationsByClaimSetId53Query : IGetApplicationsByClaimSetIdQuery +public class GetApplicationsByClaimSetIdQuery : IGetApplicationsByClaimSetIdQuery { private readonly ISecurityContext _securityContext; private readonly IUsersContext _usersContext; - public GetApplicationsByClaimSetId53Query(ISecurityContext securityContext, IUsersContext usersContext) + public GetApplicationsByClaimSetIdQuery(ISecurityContext securityContext, IUsersContext usersContext) { _securityContext = securityContext; _usersContext = usersContext; } - public IEnumerable Execute(int securityContextClaimSetId) + public IEnumerable Execute(int claimSetId) { - var claimSetName = GetClaimSetNameById(securityContextClaimSetId); + var claimSetName = GetClaimSetNameById(claimSetId); return GetApplicationsByClaimSetName(claimSetName); } @@ -52,7 +42,7 @@ private IEnumerable GetApplicationsByClaimSetName(string claimSetNa .Select(x => new Application { Name = x.ApplicationName, - VendorName = x.Vendor.VendorName + VendorName = x.Vendor != null ? x.Vendor.VendorName : null }) .ToList(); } @@ -63,3 +53,9 @@ public int ExecuteCount(int claimSetId) } } +public interface IGetApplicationsByClaimSetIdQuery +{ + IEnumerable Execute(int securityContextClaimSetId); + + int ExecuteCount(int claimSetId); +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAuthStrategiesByApplicationNameQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetAuthStrategiesByApplicationNameQuery.cs similarity index 90% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAuthStrategiesByApplicationNameQuery.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetAuthStrategiesByApplicationNameQuery.cs index aeb7ed5cd..c05fcee5b 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAuthStrategiesByApplicationNameQuery.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetAuthStrategiesByApplicationNameQuery.cs @@ -3,11 +3,9 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System.Collections.Generic; -using System.Linq; -using EdFi.Security.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; public class GetAuthStrategiesByApplicationNameQuery : IGetAuthStrategiesByApplicationNameQuery { diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQuery.cs new file mode 100644 index 000000000..1318c191b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQuery.cs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public class GetClaimSetByIdQuery : IGetClaimSetByIdQuery +{ + private readonly GetClaimSetByIdQueryService _service; + + public GetClaimSetByIdQuery(GetClaimSetByIdQueryService service) + { + _service = service; + } + + public ClaimSet Execute(int securityContextClaimSetId) + { + return _service.Execute(securityContextClaimSetId); + } +} + +public interface IGetClaimSetByIdQuery +{ + ClaimSet Execute(int securityContextClaimSetId); +} + diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQueryV6Service.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQueryService.cs similarity index 77% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQueryV6Service.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQueryService.cs index 4c36ebd0e..c98ed0ed5 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQueryV6Service.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQueryService.cs @@ -4,16 +4,17 @@ // See the LICENSE and NOTICES files in the project root for more information. using System.Net; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using EdFi.Security.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; -public class GetClaimSetByIdQueryV6Service +public class GetClaimSetByIdQueryService { private readonly ISecurityContext _securityContext; - public GetClaimSetByIdQueryV6Service(ISecurityContext securityContext) + public GetClaimSetByIdQueryService(ISecurityContext securityContext) { _securityContext = securityContext; } diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQuery.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQuery.cs new file mode 100644 index 000000000..c5fde4e43 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQuery.cs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor +{ + public class GetResourcesByClaimSetIdQuery(GetResourcesByClaimSetIdQueryService service) : IGetResourcesByClaimSetIdQuery + { + private readonly GetResourcesByClaimSetIdQueryService _service = service; + + public IList AllResources(int securityContextClaimSetId) + { + IList parentResources; + + return ModelSix(); + + IList ModelSix() + { + parentResources = _service.GetParentResources(securityContextClaimSetId); + var childResources = _service.GetChildResources(securityContextClaimSetId); + GetResourcesByClaimSetIdQueryService.AddChildResourcesToParents(childResources, parentResources); + + return parentResources; + } + } + + public ResourceClaim? SingleResource(int claimSetId, int resourceClaimId) + { + var parentResources = AllResources(claimSetId).ToList(); + var parentResourceClaim = parentResources + .SingleOrDefault(x => x.Id == resourceClaimId); + var childResources = new List(); + if (parentResourceClaim == null) + { + foreach (var resourceClaims in parentResources.Select(x => x.Children)) + childResources.AddRange(resourceClaims); + return childResources.SingleOrDefault(x => x.Id == resourceClaimId); + } + + return parentResourceClaim; + } + } + + public interface IGetResourcesByClaimSetIdQuery + { + IList AllResources(int securityContextClaimSetId); + ResourceClaim? SingleResource(int claimSetId, int resourceClaimId); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQueryV6Service.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQueryService.cs similarity index 88% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQueryV6Service.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQueryService.cs index 4d2386aab..8b529c5b6 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQueryV6Service.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQueryService.cs @@ -1,291 +1,291 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using AutoMapper; -using EdFi.Security.DataAccess.Contexts; -using EdFi.Security.DataAccess.Models; -using SecurityResourceClaim = EdFi.Security.DataAccess.Models.ResourceClaim; -using SecurityAuthorizationStrategy = EdFi.Security.DataAccess.Models.AuthorizationStrategy; -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor.Extensions; -using Microsoft.EntityFrameworkCore; - -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor -{ - public class GetResourcesByClaimSetIdQueryV6Service - { - private readonly ISecurityContext _securityContext; - private readonly IMapper _mapper; - - public GetResourcesByClaimSetIdQueryV6Service(ISecurityContext securityContext, IMapper mapper) - { - _securityContext = securityContext; - _mapper = mapper; - } - - internal static void AddChildResourcesToParents(IReadOnlyList childResources, IList parentResources) - { - foreach (var childResource in childResources) - { - var parentResource = parentResources.SingleOrDefault(x => x.Id == childResource.ParentId); - if (parentResource != null) - parentResource.Children.Add(childResource); - else - { - parentResources.Add(childResource); - } - } - } - - internal IList GetParentResources(int claimSetId) - { - var dbParentResources = _securityContext.ClaimSetResourceClaimActions - .Include(x => x.ResourceClaim) - .Include(x => x.ResourceClaim.ParentResourceClaim) - .Include(x => x.Action) - .Include(x => x.AuthorizationStrategyOverrides).ThenInclude(x => x.AuthorizationStrategy) - .Where(x => x.ClaimSet.ClaimSetId == claimSetId - && x.ResourceClaim.ParentResourceClaimId == null).ToList(); - - var defaultAuthStrategies = GetDefaultAuthStrategies(dbParentResources.Select(x => x.ResourceClaim).ToList()); - var authStrategyOverrides = GetAuthStrategyOverrides(dbParentResources.ToList()); - - var parentResources = dbParentResources.GroupBy(x => x.ResourceClaim).Select(x => new ResourceClaim - { - Id = x.Key.ResourceClaimId, - Name = x.Key.ResourceName, - Create = x.Any(a => a.Action.ActionName == Action.Create.Value), - Read = x.Any(a => a.Action.ActionName == Action.Read.Value), - Update = x.Any(a => a.Action.ActionName == Action.Update.Value), - Delete = x.Any(a => a.Action.ActionName == Action.Delete.Value), - ReadChanges = x.Any(a => a.Action.ActionName == Action.ReadChanges.Value), - IsParent = true, - DefaultAuthStrategiesForCRUD = defaultAuthStrategies[x.Key.ResourceClaimId].ToArray(), - AuthStrategyOverridesForCRUD = authStrategyOverrides[x.Key.ResourceClaimId].ToArray() - }).ToList(); - - parentResources.ForEach(x => x.Children = new List()); - return parentResources; - } - - private Dictionary GetDefaultAuthStrategies(IReadOnlyCollection resourceClaims) - { - var resultDictionary = new Dictionary(); - - var defaultAuthStrategies = _securityContext.ResourceClaimActions - .Include(x => x.ResourceClaim).Include(x => x.Action).Include(x => x.AuthorizationStrategies). - ThenInclude(x => x.AuthorizationStrategy).ToList(); - - var defaultAuthStrategiesForParents = defaultAuthStrategies - .Where(x => x.ResourceClaim.ParentResourceClaimId == null).ToList(); - - var defaultAuthStrategiesForChildren = defaultAuthStrategies - .Where(x => x.ResourceClaim.ParentResourceClaimId != null).ToList(); - - foreach (var resourceClaim in resourceClaims) - { - var actions = new List(); - if (resourceClaim.ParentResourceClaimId == null) - { - var createDefaultStrategy = defaultAuthStrategiesForParents.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Create.Value)?.AuthorizationStrategies.Select(x => x.AuthorizationStrategy); - AddStrategyToParentResource(createDefaultStrategy); - var readDefaultStrategy = defaultAuthStrategiesForParents.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Read.Value)?.AuthorizationStrategies.Select(x => x.AuthorizationStrategy); - AddStrategyToParentResource(readDefaultStrategy); - var updateDefaultStrategy = defaultAuthStrategiesForParents - .SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Update.Value)?.AuthorizationStrategies.Select(x => x.AuthorizationStrategy); - AddStrategyToParentResource(updateDefaultStrategy); - var deleteDefaultStrategy = defaultAuthStrategiesForParents - .SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Delete.Value)?.AuthorizationStrategies.Select(x => x.AuthorizationStrategy); - AddStrategyToParentResource(deleteDefaultStrategy); - var readChangesDefaultStrategy = defaultAuthStrategiesForParents - .SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.ReadChanges.Value)?.AuthorizationStrategies.Select(x => x.AuthorizationStrategy); - AddStrategyToParentResource(readChangesDefaultStrategy); - - void AddStrategyToParentResource(IEnumerable? defaultStrategy) - { - actions.Add(defaultStrategy != null ? new ClaimSetResourceClaimActionAuthStrategies() - { - AuthorizationStrategies = defaultStrategy.Select(x => _mapper.Map(x)).ToList()! - } : null); - } - } - else - { - var createDefaultStrategies = defaultAuthStrategiesForChildren.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Create.Value)?.AuthorizationStrategies.Select(x => x.AuthorizationStrategy); - actions = AddStrategyToChildResource(createDefaultStrategies, Action.Create); - - var readDefaultStrategies = defaultAuthStrategiesForChildren.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Read.Value)?.AuthorizationStrategies.Select(x => x.AuthorizationStrategy); - actions = AddStrategyToChildResource(readDefaultStrategies, Action.Read); - - var updateDefaultStrategies = defaultAuthStrategiesForChildren.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Update.Value)?.AuthorizationStrategies.Select(x => x.AuthorizationStrategy); - actions = AddStrategyToChildResource(updateDefaultStrategies, Action.Update); - - var deleteDefaultStrategies = defaultAuthStrategiesForChildren.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Delete.Value)?.AuthorizationStrategies.Select(x => x.AuthorizationStrategy); - actions = AddStrategyToChildResource(deleteDefaultStrategies, Action.Delete); - - var readChangesDefaultStrategy = defaultAuthStrategiesForChildren.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.ReadChanges.Value)?.AuthorizationStrategies.Select(x => x.AuthorizationStrategy); - actions = AddStrategyToChildResource(readChangesDefaultStrategy, Action.ReadChanges); - - List AddStrategyToChildResource(IEnumerable? defaultStrategies, Action action) - { - if (defaultStrategies == null) - { - defaultStrategies = defaultAuthStrategiesForParents.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ParentResourceClaimId && - x.Action.ActionName == action.Value)?.AuthorizationStrategies.Select(x => x.AuthorizationStrategy); - - if (defaultStrategies != null) - { - var mappedStrategies = defaultStrategies.Select(x => - { - var value = _mapper.Map(x); - if (value != null) - value.IsInheritedFromParent = true; - return value; - }); - - actions.Add(new ClaimSetResourceClaimActionAuthStrategies() - { - AuthorizationStrategies = mappedStrategies.ToArray() - }); - } - } - else - { - var mappedStrategies = defaultStrategies.Select(x => _mapper.Map(x)); - actions.Add(new ClaimSetResourceClaimActionAuthStrategies() - { - AuthorizationStrategies = mappedStrategies.ToArray() - }); - } - - return actions; - } - } - - resultDictionary[resourceClaim.ResourceClaimId] = actions.ToArray() as ClaimSetResourceClaimActionAuthStrategies[]; - } - - return resultDictionary; - } - - private Dictionary GetAuthStrategyOverrides(List resourceClaims) - { - var resultDictionary = new Dictionary(); - resourceClaims = - new List(resourceClaims.OrderBy(i => new List { Action.Create.Value, Action.Read.Value, Action.Update.Value, Action.Delete.Value, Action.ReadChanges.Value }.IndexOf(i.Action.ActionName))); - foreach (var resourceClaim in resourceClaims) - { - List? authStrategies = null; - if (resourceClaim.ResourceClaim.ParentResourceClaim == null) - { - authStrategies = _mapper.Map>(resourceClaim.AuthorizationStrategyOverrides is not null && resourceClaim.AuthorizationStrategyOverrides.Any() ? - resourceClaim.AuthorizationStrategyOverrides.Select(x => x.AuthorizationStrategy) : null); - } - else - { - var parentResources = _securityContext.ClaimSetResourceClaimActions - .Include(x => x.ResourceClaim) - .Include(x => x.ClaimSet) - .Include(x => x.Action) - .Include(x => x.AuthorizationStrategyOverrides).ThenInclude(x => x.AuthorizationStrategy).ToList(); - var parentResourceOverride = parentResources.SingleOrDefault(x => x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaim.ParentResourceClaimId - && x.ClaimSet.ClaimSetId == resourceClaim.ClaimSet.ClaimSetId - && x.Action.ActionId == resourceClaim.Action.ActionId); - if (parentResourceOverride?.AuthorizationStrategyOverrides != null && parentResourceOverride.AuthorizationStrategyOverrides.Any()) - { - authStrategies = - _mapper.Map>(parentResourceOverride.AuthorizationStrategyOverrides.Select(x => x.AuthorizationStrategy)); - if (authStrategies != null) - { - authStrategies.ForEach(a => a.IsInheritedFromParent = true); - } - } - - if (resourceClaim.AuthorizationStrategyOverrides != null && resourceClaim.AuthorizationStrategyOverrides.Any()) - { - authStrategies = _mapper.Map>(resourceClaim.AuthorizationStrategyOverrides.Select(x => x.AuthorizationStrategy)); - } - } - - if (resultDictionary.ContainsKey(resourceClaim.ResourceClaim.ResourceClaimId)) - { - if (authStrategies != null) - { - foreach (var authStrategy in authStrategies) - { - resultDictionary[resourceClaim.ResourceClaim.ResourceClaimId].AddAuthorizationStrategyOverrides(resourceClaim.Action.ActionName, authStrategy); - } - } - } - else - { - resultDictionary[resourceClaim.ResourceClaim.ResourceClaimId] = new ClaimSetResourceClaimActionAuthStrategies[5]; - if (authStrategies != null) - { - foreach (var authStrategy in authStrategies) - { - resultDictionary[resourceClaim.ResourceClaim.ResourceClaimId].AddAuthorizationStrategyOverrides(resourceClaim.Action.ActionName, authStrategy); - } - } - } - - } - return resultDictionary; - } - - internal IReadOnlyList GetChildResources(int claimSetId) - { - var dbChildResources = - _securityContext.ClaimSetResourceClaimActions - .Include(x => x.ResourceClaim) - .Include(x => x.Action) - .Include(x => x.AuthorizationStrategyOverrides) - .Where(x => x.ClaimSet.ClaimSetId == claimSetId - && x.ResourceClaim.ParentResourceClaimId != null).ToList(); - var defaultAuthStrategies = GetDefaultAuthStrategies(dbChildResources.Select(x => x.ResourceClaim).ToList()); - var authStrategyOverrides = GetAuthStrategyOverrides(dbChildResources.ToList()); - - var childResources = dbChildResources.GroupBy(x => x.ResourceClaim) - .Select(x => new ResourceClaim - { - Id = x.Key.ResourceClaimId, - ParentId = x.Key.ParentResourceClaimId ?? 0, - Name = x.Key.ResourceName, - Create = x.Any(a => a.Action.ActionName == Action.Create.Value), - Read = x.Any(a => a.Action.ActionName == Action.Read.Value), - Update = x.Any(a => a.Action.ActionName == Action.Update.Value), - Delete = x.Any(a => a.Action.ActionName == Action.Delete.Value), - ReadChanges = x.Any(a => a.Action.ActionName == Action.ReadChanges.Value), - IsParent = false, - DefaultAuthStrategiesForCRUD = defaultAuthStrategies[x.Key.ResourceClaimId], - AuthStrategyOverridesForCRUD = authStrategyOverrides.Keys.Any(p => p == x.Key.ResourceClaimId) ? authStrategyOverrides[x.Key.ResourceClaimId] : Array.Empty(), - }).ToList(); - return childResources; - } - } -} - - +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor.Extensions; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; +using Microsoft.EntityFrameworkCore; +using SecurityAuthorizationStrategy = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.AuthorizationStrategy; +using SecurityResourceClaim = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.ResourceClaim; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor +{ + public class GetResourcesByClaimSetIdQueryService + { + private readonly ISecurityContext _securityContext; + private readonly IMapper _mapper; + + public GetResourcesByClaimSetIdQueryService(ISecurityContext securityContext, IMapper mapper) + { + _securityContext = securityContext; + _mapper = mapper; + } + + internal static void AddChildResourcesToParents(IReadOnlyList childResources, IList parentResources) + { + foreach (var childResource in childResources) + { + var parentResource = parentResources.SingleOrDefault(x => x.Id == childResource.ParentId); + if (parentResource != null) + parentResource.Children.Add(childResource); + else + { + parentResources.Add(childResource); + } + } + } + + internal IList GetParentResources(int claimSetId) + { + var dbParentResources = _securityContext.ClaimSetResourceClaimActions + .Include(x => x.ResourceClaim) + .Include(x => x.ResourceClaim.ParentResourceClaim) + .Include(x => x.Action) + .Include(x => x.AuthorizationStrategyOverrides!) + .ThenInclude(x => x.AuthorizationStrategy) + .Where(x => x.ClaimSet.ClaimSetId == claimSetId + && x.ResourceClaim.ParentResourceClaimId == null).ToList(); + + var defaultAuthStrategies = GetDefaultAuthStrategies(dbParentResources.Select(x => x.ResourceClaim).ToList()); + var authStrategyOverrides = GetAuthStrategyOverrides(dbParentResources.ToList()); + + var parentResources = dbParentResources.GroupBy(x => x.ResourceClaim).Select(x => new ResourceClaim + { + Id = x.Key.ResourceClaimId, + Name = x.Key.ResourceName, + Create = x.Any(a => a.Action.ActionName == Action.Create.Value), + Read = x.Any(a => a.Action.ActionName == Action.Read.Value), + Update = x.Any(a => a.Action.ActionName == Action.Update.Value), + Delete = x.Any(a => a.Action.ActionName == Action.Delete.Value), + ReadChanges = x.Any(a => a.Action.ActionName == Action.ReadChanges.Value), + IsParent = true, + DefaultAuthStrategiesForCRUD = defaultAuthStrategies[x.Key.ResourceClaimId].ToArray(), + AuthStrategyOverridesForCRUD = authStrategyOverrides[x.Key.ResourceClaimId].ToArray() + }).ToList(); + + parentResources.ForEach(x => x.Children = new List()); + return parentResources; + } + + private Dictionary GetDefaultAuthStrategies(IReadOnlyCollection resourceClaims) + { + var resultDictionary = new Dictionary(); + + var defaultAuthStrategies = _securityContext.ResourceClaimActions + .Include(x => x.ResourceClaim).Include(x => x.Action).Include(x => x.AuthorizationStrategies!) + .ThenInclude(x => x.AuthorizationStrategy).ToList(); + + var defaultAuthStrategiesForParents = defaultAuthStrategies + .Where(x => x.ResourceClaim.ParentResourceClaimId == null).ToList(); + + var defaultAuthStrategiesForChildren = defaultAuthStrategies + .Where(x => x.ResourceClaim.ParentResourceClaimId != null).ToList(); + + foreach (var resourceClaim in resourceClaims) + { + var actions = new List(); + if (resourceClaim.ParentResourceClaimId == null) + { + var createDefaultStrategy = defaultAuthStrategiesForParents.SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && + x.Action.ActionName == Action.Create.Value)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + AddStrategyToParentResource(createDefaultStrategy); + var readDefaultStrategy = defaultAuthStrategiesForParents.SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && + x.Action.ActionName == Action.Read.Value)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + AddStrategyToParentResource(readDefaultStrategy); + var updateDefaultStrategy = defaultAuthStrategiesForParents + .SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && + x.Action.ActionName == Action.Update.Value)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + AddStrategyToParentResource(updateDefaultStrategy); + var deleteDefaultStrategy = defaultAuthStrategiesForParents + .SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && + x.Action.ActionName == Action.Delete.Value)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + AddStrategyToParentResource(deleteDefaultStrategy); + var readChangesDefaultStrategy = defaultAuthStrategiesForParents + .SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && + x.Action.ActionName == Action.ReadChanges.Value)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + AddStrategyToParentResource(readChangesDefaultStrategy); + + void AddStrategyToParentResource(IEnumerable? defaultStrategy) + { + actions.Add(defaultStrategy != null ? new ClaimSetResourceClaimActionAuthStrategies() + { + AuthorizationStrategies = defaultStrategy.Select(x => _mapper.Map(x)).ToList()! + } : null); + } + } + else + { + var createDefaultStrategies = defaultAuthStrategiesForChildren.SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && + x.Action.ActionName == Action.Create.Value)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + actions = AddStrategyToChildResource(createDefaultStrategies, Action.Create); + + var readDefaultStrategies = defaultAuthStrategiesForChildren.SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && + x.Action.ActionName == Action.Read.Value)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + actions = AddStrategyToChildResource(readDefaultStrategies, Action.Read); + + var updateDefaultStrategies = defaultAuthStrategiesForChildren.SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && + x.Action.ActionName == Action.Update.Value)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + actions = AddStrategyToChildResource(updateDefaultStrategies, Action.Update); + + var deleteDefaultStrategies = defaultAuthStrategiesForChildren.SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && + x.Action.ActionName == Action.Delete.Value)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + actions = AddStrategyToChildResource(deleteDefaultStrategies, Action.Delete); + + var readChangesDefaultStrategy = defaultAuthStrategiesForChildren.SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && + x.Action.ActionName == Action.ReadChanges.Value)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + actions = AddStrategyToChildResource(readChangesDefaultStrategy, Action.ReadChanges); + + List AddStrategyToChildResource(IEnumerable? defaultStrategies, Action action) + { + if (defaultStrategies == null) + { + defaultStrategies = defaultAuthStrategiesForParents.SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ParentResourceClaimId && + x.Action.ActionName == action.Value)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + + if (defaultStrategies != null) + { + var mappedStrategies = defaultStrategies.Select(x => + { + var value = _mapper.Map(x); + if (value != null) + value.IsInheritedFromParent = true; + return value; + }); + + actions.Add(new ClaimSetResourceClaimActionAuthStrategies() + { + AuthorizationStrategies = mappedStrategies.ToArray() + }); + } + } + else + { + var mappedStrategies = defaultStrategies.Select(x => _mapper.Map(x)); + actions.Add(new ClaimSetResourceClaimActionAuthStrategies() + { + AuthorizationStrategies = mappedStrategies.ToArray() + }); + } + + return actions; + } + } + + resultDictionary[resourceClaim.ResourceClaimId] = actions.ToArray() as ClaimSetResourceClaimActionAuthStrategies[]; + } + + return resultDictionary; + } + + private Dictionary GetAuthStrategyOverrides(List resourceClaims) + { + var resultDictionary = new Dictionary(); + resourceClaims = + new List(resourceClaims.OrderBy(i => new List { Action.Create.Value, Action.Read.Value, Action.Update.Value, Action.Delete.Value, Action.ReadChanges.Value }.IndexOf(i.Action.ActionName))); + foreach (var resourceClaim in resourceClaims) + { + List? authStrategies = null; + if (resourceClaim.ResourceClaim.ParentResourceClaim == null) + { + authStrategies = _mapper.Map>(resourceClaim.AuthorizationStrategyOverrides is not null && resourceClaim.AuthorizationStrategyOverrides.Any() ? + resourceClaim.AuthorizationStrategyOverrides.Select(x => x.AuthorizationStrategy) : null); + } + else + { + var parentResources = _securityContext.ClaimSetResourceClaimActions + .Include(x => x.ResourceClaim) + .Include(x => x.ClaimSet) + .Include(x => x.Action) + .Include(x => x.AuthorizationStrategyOverrides!).ThenInclude(x => x.AuthorizationStrategy).ToList(); + var parentResourceOverride = parentResources.SingleOrDefault(x => x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaim.ParentResourceClaimId + && x.ClaimSet.ClaimSetId == resourceClaim.ClaimSet.ClaimSetId + && x.Action.ActionId == resourceClaim.Action.ActionId); + if (parentResourceOverride?.AuthorizationStrategyOverrides != null && parentResourceOverride.AuthorizationStrategyOverrides.Any()) + { + authStrategies = + _mapper.Map>(parentResourceOverride.AuthorizationStrategyOverrides.Select(x => x.AuthorizationStrategy)); + if (authStrategies != null) + { + authStrategies.ForEach(a => a.IsInheritedFromParent = true); + } + } + + if (resourceClaim.AuthorizationStrategyOverrides != null && resourceClaim.AuthorizationStrategyOverrides.Any()) + { + authStrategies = _mapper.Map>(resourceClaim.AuthorizationStrategyOverrides.Select(x => x.AuthorizationStrategy)); + } + } + + if (resultDictionary.ContainsKey(resourceClaim.ResourceClaim.ResourceClaimId)) + { + if (authStrategies != null) + { + foreach (var authStrategy in authStrategies) + { + resultDictionary[resourceClaim.ResourceClaim.ResourceClaimId].AddAuthorizationStrategyOverrides(resourceClaim.Action.ActionName, authStrategy); + } + } + } + else + { + resultDictionary[resourceClaim.ResourceClaim.ResourceClaimId] = new ClaimSetResourceClaimActionAuthStrategies[5]; + if (authStrategies != null) + { + foreach (var authStrategy in authStrategies) + { + resultDictionary[resourceClaim.ResourceClaim.ResourceClaimId].AddAuthorizationStrategyOverrides(resourceClaim.Action.ActionName, authStrategy); + } + } + } + + } + return resultDictionary; + } + + internal IReadOnlyList GetChildResources(int claimSetId) + { + var dbChildResources = + _securityContext.ClaimSetResourceClaimActions + .Include(x => x.ResourceClaim) + .Include(x => x.Action) + .Include(x => x.AuthorizationStrategyOverrides) + .Where(x => x.ClaimSet.ClaimSetId == claimSetId + && x.ResourceClaim.ParentResourceClaimId != null).ToList(); + var defaultAuthStrategies = GetDefaultAuthStrategies(dbChildResources.Select(x => x.ResourceClaim).ToList()); + var authStrategyOverrides = GetAuthStrategyOverrides(dbChildResources.ToList()); + + var childResources = dbChildResources.GroupBy(x => x.ResourceClaim) + .Select(x => new ResourceClaim + { + Id = x.Key.ResourceClaimId, + ParentId = x.Key.ParentResourceClaimId ?? 0, + Name = x.Key.ResourceName, + Create = x.Any(a => a.Action.ActionName == Action.Create.Value), + Read = x.Any(a => a.Action.ActionName == Action.Read.Value), + Update = x.Any(a => a.Action.ActionName == Action.Update.Value), + Delete = x.Any(a => a.Action.ActionName == Action.Delete.Value), + ReadChanges = x.Any(a => a.Action.ActionName == Action.ReadChanges.Value), + IsParent = false, + DefaultAuthStrategiesForCRUD = defaultAuthStrategies[x.Key.ResourceClaimId], + AuthStrategyOverridesForCRUD = authStrategyOverrides.Keys.Any(p => p == x.Key.ResourceClaimId) ? authStrategyOverrides[x.Key.ResourceClaimId] : Array.Empty(), + }).ToList(); + return childResources; + } + } +} + + diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyCommand.cs new file mode 100644 index 000000000..cb58c9bfa --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyCommand.cs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public class OverrideDefaultAuthorizationStrategyCommand +{ + private readonly OverrideDefaultAuthorizationStrategyService _service; + + public OverrideDefaultAuthorizationStrategyCommand(OverrideDefaultAuthorizationStrategyService service) + { + _service = service; + } + + public void Execute(IOverrideDefaultAuthorizationStrategyModel model) + { + _service.Execute(model); + } +} + +public interface IOverrideDefaultAuthorizationStrategyModel +{ + int ClaimSetId { get; } + int ResourceClaimId { get; } + int[]? AuthorizationStrategyForCreate { get; } + int[]? AuthorizationStrategyForRead { get; } + int[]? AuthorizationStrategyForUpdate { get; } + int[]? AuthorizationStrategyForDelete { get; } + int[]? AuthorizationStrategyForReadChanges { get; } +} + diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyV6Service.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyService.cs similarity index 85% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyV6Service.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyService.cs index 8e167b3c9..155f68f16 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyV6Service.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyService.cs @@ -3,17 +3,17 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Security.DataAccess.Contexts; -using EdFi.Security.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; using Microsoft.EntityFrameworkCore; -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; -public class OverrideDefaultAuthorizationStrategyV6Service +public class OverrideDefaultAuthorizationStrategyService { private readonly ISecurityContext _context; - public OverrideDefaultAuthorizationStrategyV6Service(ISecurityContext context) + public OverrideDefaultAuthorizationStrategyService(ISecurityContext context) { _context = context; } @@ -24,7 +24,7 @@ public void Execute(IOverrideDefaultAuthorizationStrategyModel model) .Include(x => x.ResourceClaim) .Include(x => x.Action) .Include(x => x.ClaimSet) - .Include(x => x.AuthorizationStrategyOverrides).ThenInclude(x => x.AuthorizationStrategy) + .Include(x => x.AuthorizationStrategyOverrides!).ThenInclude(x => x.AuthorizationStrategy) .Where( x => x.ResourceClaim.ResourceClaimId == model.ResourceClaimId && x.ClaimSet.ClaimSetId == model.ClaimSetId) @@ -41,7 +41,7 @@ public void Execute(IOverrideDefaultAuthorizationStrategyModel model) .Include(x => x.ResourceClaim) .Include(x => x.Action) .Include(x => x.ClaimSet) - .Include(x => x.AuthorizationStrategyOverrides).ThenInclude(x => x.AuthorizationStrategy) + .Include(x => x.AuthorizationStrategyOverrides!).ThenInclude(x => x.AuthorizationStrategy) .Where( x => x.ResourceClaim.ResourceClaimId == parentResourceClaimId && x.ClaimSet.ClaimSetId == model.ClaimSetId) @@ -49,7 +49,7 @@ public void Execute(IOverrideDefaultAuthorizationStrategyModel model) } var authorizationStrategiesDictionary = - new Dictionary(); + new Dictionary(); foreach (var authStrategy in _context.AuthorizationStrategies.ToList()) { @@ -90,7 +90,7 @@ private List RemoveOverrides( model.AuthorizationStrategyForDelete.Length == 0) { claimSetResourceClaim.AuthorizationStrategyOverrides = null; - } + } else if (claimSetResourceClaim.Action.ActionName == Action.ReadChanges.Value && model.AuthorizationStrategyForReadChanges != null && model.AuthorizationStrategyForReadChanges.Length == 0) { @@ -119,7 +119,7 @@ void RemoveClaimSetResourceClaimActionAuthorizationStrategyOverrides(ClaimSetRes private static void AddOverrides(IOverrideDefaultAuthorizationStrategyModel model, IEnumerable resourceClaimsToEdit, - Dictionary authorizationStrategiesDictionary, + Dictionary authorizationStrategiesDictionary, List parentResourceClaims) { var claimSetResourceClaims = resourceClaimsToEdit.ToList(); @@ -154,7 +154,7 @@ private static void AddOverrides(IOverrideDefaultAuthorizationStrategyModel mode SetAuthorizationStrategyOverrides( claimSetResourceClaim, parentResourceClaims, model.AuthorizationStrategyForDelete, authorizationStrategiesDictionary, Action.Delete.Value); - } + } else if (claimSetResourceClaim.Action.ActionName == Action.ReadChanges.Value && model.AuthorizationStrategyForReadChanges != null && model.AuthorizationStrategyForReadChanges.Length != 0) { @@ -166,29 +166,36 @@ private static void AddOverrides(IOverrideDefaultAuthorizationStrategyModel mode } private static void SetAuthorizationStrategyOverrides( - ClaimSetResourceClaimAction claimSetResourceClaimAction, - List parentResourceClaims, int[] authorizationStrategyValues, - Dictionary - authorizationStrategiesDictionary, string actionName) - { - foreach (var authStrategyId in authorizationStrategyValues.Where(x => x != 0)) - { - var authStrategyOverride = new ClaimSetResourceClaimActionAuthorizationStrategyOverrides() - { AuthorizationStrategy = authorizationStrategiesDictionary[authStrategyId] }; - - if (parentResourceClaims.Any() && parentResourceClaims.SingleOrDefault( - x => - x.Action.ActionName == actionName && x.AuthorizationStrategyOverrides != null && - x.AuthorizationStrategyOverrides.Any() && - x.AuthorizationStrategyOverrides.SingleOrDefault()?.AuthorizationStrategyId == - authStrategyId) == null) - { - claimSetResourceClaimAction.AuthorizationStrategyOverrides.Add(authStrategyOverride); - } - else if (!parentResourceClaims.Any()) - { - claimSetResourceClaimAction.AuthorizationStrategyOverrides.Add(authStrategyOverride); - } + ClaimSetResourceClaimAction claimSetResourceClaimAction, + List parentResourceClaims, int[] authorizationStrategyValues, + Dictionary authorizationStrategiesDictionary, string actionName) + { + foreach (var authStrategyId in authorizationStrategyValues.Where(x => x != 0)) + { + var authStrategyOverride = new ClaimSetResourceClaimActionAuthorizationStrategyOverrides() + { + AuthorizationStrategy = authorizationStrategiesDictionary[authStrategyId], + ClaimSetResourceClaimAction = claimSetResourceClaimAction + }; + + if (claimSetResourceClaimAction.AuthorizationStrategyOverrides == null) + { + claimSetResourceClaimAction.AuthorizationStrategyOverrides = new List(); + } + + if (parentResourceClaims.Any() && parentResourceClaims.SingleOrDefault( + x => + x.Action.ActionName == actionName && x.AuthorizationStrategyOverrides != null && + x.AuthorizationStrategyOverrides.Any() && + x.AuthorizationStrategyOverrides.SingleOrDefault()?.AuthorizationStrategyId == + authStrategyId) == null) + { + claimSetResourceClaimAction.AuthorizationStrategyOverrides.Add(authStrategyOverride); + } + else if (!parentResourceClaims.Any()) + { + claimSetResourceClaimAction.AuthorizationStrategyOverrides.Add(authStrategyOverride); + } } } } diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/ResourceClaim.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/ResourceClaim.cs new file mode 100644 index 000000000..161844d44 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/ResourceClaim.cs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using Newtonsoft.Json; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; + +public class ResourceClaim +{ + public int Id { get; set; } + public int ParentId { get; set; } + public string? ParentName { get; set; } + public string? Name { get; set; } + public bool Create { get; set; } + public bool Read { get; set; } + public bool Update { get; set; } + public bool Delete { get; set; } + public bool ReadChanges { get; set; } + [JsonIgnore] + public bool IsParent { get; set; } + public ClaimSetResourceClaimActionAuthStrategies?[] DefaultAuthStrategiesForCRUD { get; set; } = Array.Empty(); + public ClaimSetResourceClaimActionAuthStrategies?[] AuthStrategyOverridesForCRUD { get; set; } = Array.Empty(); + public List Children { get; set; } = new(); + + public ResourceClaim() + { + Children = new List(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommand.cs new file mode 100644 index 000000000..5991ead56 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommand.cs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor +{ + public class UpdateResourcesOnClaimSetCommand + { + private readonly UpdateResourcesOnClaimSetCommandService _service; + + public UpdateResourcesOnClaimSetCommand(UpdateResourcesOnClaimSetCommandService service) + { + _service = service; + } + + public void Execute(IUpdateResourcesOnClaimSetModel model) + { + _service.Execute(model); + } + } + + public interface IUpdateResourcesOnClaimSetModel + { + int ClaimSetId { get; } + List? ResourceClaims { get; } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommandV6Service.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommandService.cs similarity index 70% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommandV6Service.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommandService.cs index e49134a5c..89f1a5d49 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommandV6Service.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommandService.cs @@ -3,18 +3,19 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Security.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; using Microsoft.EntityFrameworkCore; -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services.ClaimSetEditor { - public class UpdateResourcesOnClaimSetCommandV6Service + public class UpdateResourcesOnClaimSetCommandService { private readonly ISecurityContext _context; private readonly AddOrEditResourcesOnClaimSetCommand _addOrEditResourcesOnClaimSetCommand; - public UpdateResourcesOnClaimSetCommandV6Service(ISecurityContext context, + public UpdateResourcesOnClaimSetCommandService(ISecurityContext context, AddOrEditResourcesOnClaimSetCommand addOrEditResourcesOnClaimSetCommand) { _context = context; @@ -24,14 +25,15 @@ public UpdateResourcesOnClaimSetCommandV6Service(ISecurityContext context, public void Execute(IUpdateResourcesOnClaimSetModel model) { var resourceClaimsForClaimSet = - _context - .ClaimSetResourceClaimActions - .Include(x => x.AuthorizationStrategyOverrides).ThenInclude(x => x.AuthorizationStrategy) + _context + .ClaimSetResourceClaimActions + .Include(x => x.AuthorizationStrategyOverrides!).ThenInclude(x => x.AuthorizationStrategy) .Where(x => x.ClaimSet.ClaimSetId == model.ClaimSetId).ToList(); _context.ClaimSetResourceClaimActions.RemoveRange(resourceClaimsForClaimSet); _context.SaveChanges(); - if (model.ResourceClaims == null) return; + if (model.ResourceClaims == null) + return; _addOrEditResourcesOnClaimSetCommand.Execute(model.ClaimSetId, model.ResourceClaims); } diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/OdsApiValidator.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/OdsApiValidator.cs new file mode 100644 index 000000000..e7099f355 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/OdsApiValidator.cs @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NJsonSchema; + +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services; + +public interface IOdsApiValidator +{ + Task Validate(string apiServerUrl); +} + +public class OdsApiValidatorResult +{ + public bool IsValidOdsApi { get; set; } + + public Version? Version { get; set; } + + public Exception? Exception { get; set; } +} + +public class OdsApiValidator(ISimpleGetRequest getRequest) : IOdsApiValidator +{ + private readonly ISimpleGetRequest _getRequest = getRequest; + + public async Task Validate(string apiServerUrl) + { + try + { + var contentAsString = await _getRequest.DownloadString(apiServerUrl); + + var schemaJson = GetSchemaJson(); + + var schema = await JsonSchema.FromJsonAsync(schemaJson); + + var parsedContent = ParseJson(contentAsString); + + var errors = schema.Validate(parsedContent); + + if (errors.Count == 0 && parsedContent.TryGetValue("version", out var version)) + { + return new OdsApiValidatorResult + { + Version = Version.Parse(version.Value()!), + IsValidOdsApi = true + }; + } + + return errors.Count == 0 + ? new OdsApiValidatorResult { IsValidOdsApi = true } + : InvalidOdsApiValidatorResult("The API provided does not have a valid root JSON document."); + } + catch (InvalidOperationException exception) + { + return InvalidOdsApiValidatorResult(exception.Message); + } + catch (JsonException exception) + { + return InvalidOdsApiValidatorResult($"The API provided does not have a valid root JSON document. JSON Parser Exception encountered: {exception.Message}"); + } + catch (HttpRequestException exception) + { + return InvalidOdsApiValidatorResult(exception.Message, exception.StatusCode ?? HttpStatusCode.ServiceUnavailable); + } + catch (Exception exception) + { + return InvalidOdsApiValidatorResult(exception.Message); + } + + string GetSchemaJson() => @"{ + ""type"": ""object"", + ""properties"": { + ""version"": { + ""type"": ""string"" + }, + ""informationalVersion"": { + ""type"": ""string"" + }, + ""suite"": { + ""type"": ""string"" + }, + ""build"": { + ""type"": ""string"" + }, + ""apiMode"": { + ""type"": ""string"" + }, + ""dataModels"": { + ""type"": ""array"", + ""items"": [ + { + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"", + ""pattern"": ""Ed-Fi"" + }, + ""version"": { + ""type"": ""string"" + }, + ""informationalVersion"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""name"", + ""version"" + ] + } + ] + }, + ""urls"": { + ""type"": ""object"", + ""properties"": { + ""dependencies"": { + ""type"": ""string"" + }, + ""openApiMetadata"": { + ""type"": ""string"" + }, + ""oauth"": { + ""type"": ""string"" + }, + ""dataManagementApi"": { + ""type"": ""string"" + }, + ""xsdMetadata"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""dependencies"", + ""openApiMetadata"", + ""oauth"", + ""dataManagementApi"", + ""xsdMetadata"" + ] + } + }, + ""required"": [ + ""version"", + ""informationalVersion"", + ""build"", + ""apiMode"" + ] + }"; + + OdsApiValidatorResult InvalidOdsApiValidatorResult(string exceptionMessage, HttpStatusCode statusCode = HttpStatusCode.ServiceUnavailable) + { + var message = + $"Invalid ODS API configured. Please verify that the Production ODS API Url ({apiServerUrl}) is configured correctly."; + if (!string.IsNullOrEmpty(exceptionMessage)) + { + message += $" Error Details: {exceptionMessage}"; + } + + return new OdsApiValidatorResult + { + IsValidOdsApi = false, + Exception = new OdsApiConnectionException( + statusCode, "Invalid ODS API configured.", message) + { AllowFeedback = false } + }; + } + } + + private static JObject ParseJson(string contentAsString) + { + return string.IsNullOrEmpty(contentAsString) + ? [] + : JObject.Parse(contentAsString); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/SimpleGetRequest.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/SimpleGetRequest.cs similarity index 98% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/SimpleGetRequest.cs rename to Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/SimpleGetRequest.cs index fc585976b..70378f993 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/SimpleGetRequest.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Services/SimpleGetRequest.cs @@ -8,7 +8,7 @@ using System.Net.Http; using System.Threading.Tasks; -namespace EdFi.Ods.AdminApi.Infrastructure.Services; +namespace EdFi.Ods.AdminApi.V1.Infrastructure.Services; public interface ISimpleGetRequest { diff --git a/Application/EdFi.Ods.AdminApi.V1/Properties/launchSettings.json b/Application/EdFi.Ods.AdminApi.V1/Properties/launchSettings.json new file mode 100644 index 000000000..04872a951 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "EdFi.Ods.AdminApi.V1": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:63537;http://localhost:63538" + } + } +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Caching/IInstanceSecurityRepositoryCache.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Caching/IInstanceSecurityRepositoryCache.cs new file mode 100644 index 000000000..4f337f68e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Caching/IInstanceSecurityRepositoryCache.cs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Security.DataAccess.V1.Caching; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Caching +{ + public interface IInstanceSecurityRepositoryCache + { + InstanceSecurityRepositoryCacheObject GetSecurityRepository(string instanceId); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Caching/InstanceSecurityRepositoryCacheObject.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Caching/InstanceSecurityRepositoryCacheObject.cs new file mode 100644 index 000000000..67565e195 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Caching/InstanceSecurityRepositoryCacheObject.cs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; +using Action = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Action; + +namespace EdFi.Security.DataAccess.V1.Caching +{ + public class InstanceSecurityRepositoryCacheObject + { + public Application Application { get; } + + public List Actions { get; } + + public List ClaimSets { get; } + + public List ResourceClaims { get; } + + public List AuthorizationStrategies { get; } + + public List ClaimSetResourceClaimActions { get; } + + public List ResourceClaimActions { get; } + + + public InstanceSecurityRepositoryCacheObject( + Application application, + List actions, + List claimSets, + List resourceClaims, + List authorizationStrategies, + List claimSetResourceClaimActions, + List resourceClaimActions) + { + Application = application; + Actions = actions; + ClaimSets = claimSets; + ResourceClaims = resourceClaims; + AuthorizationStrategies = authorizationStrategies; + ClaimSetResourceClaimActions = claimSetResourceClaimActions; + ResourceClaimActions = resourceClaimActions; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/ISecurityContext.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/ISecurityContext.cs new file mode 100644 index 000000000..79937d84c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/ISecurityContext.cs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; +using Action = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Action; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts +{ + public interface ISecurityContext : IDisposable + { + DbSet Applications { get; set; } + + DbSet Actions { get; set; } + + DbSet AuthorizationStrategies { get; set; } + + DbSet ClaimSets { get; set; } + + DbSet ClaimSetResourceClaimActions { get; set; } + + DbSet ResourceClaims { get; set; } + + DbSet ResourceClaimActions { get; set; } + + DbSet ClaimSetResourceClaimActionAuthorizationStrategyOverrides { get; set; } + + DbSet ResourceClaimActionAuthorizationStrategies { get; set; } + + int SaveChanges(); + + Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/ISecurityContextFactory.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/ISecurityContextFactory.cs new file mode 100644 index 000000000..95ff885b0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/ISecurityContextFactory.cs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts +{ + public interface ISecurityContextFactory + { + ISecurityContext CreateContext(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/PostgresSecurityContext.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/PostgresSecurityContext.cs new file mode 100644 index 000000000..974fbf73c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/PostgresSecurityContext.cs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq; +using EdFi.Common.Utils.Extensions; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts +{ + public class PostgresSecurityContext : SecurityContext + { + public PostgresSecurityContext(DbContextOptions options) + : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Model.GetEntityTypes().ForEach(entityType => + entityType.SetSchema("dbo")); + + //Fixes mapping error when using EFC + modelBuilder.Model.GetEntityTypes().Single(e => e.ClrType.Name == nameof(ClaimSet)) + .GetProperty("ApplicationId") + .SetColumnName("application_applicationid"); + + modelBuilder.Model.GetEntityTypes().Single(e => e.ClrType.Name == nameof(ResourceClaim)) + .GetProperty("ApplicationId") + .SetColumnName("application_applicationid"); + + modelBuilder.Model.GetEntityTypes().Single(e => e.ClrType.Name == nameof(AuthorizationStrategy)) + .GetProperty("ApplicationId") + .SetColumnName("application_applicationid"); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/SecurityContext.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/SecurityContext.cs new file mode 100644 index 000000000..dc38d5621 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/SecurityContext.cs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; +using Microsoft.EntityFrameworkCore; +using Action = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Action; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts +{ + public abstract class SecurityContext : DbContext, ISecurityContext + { + protected SecurityContext(DbContextOptions options) + : base(options) { } + + public DbSet Applications { get; set; } + + public DbSet Actions { get; set; } + + public DbSet AuthorizationStrategies { get; set; } + + public DbSet ClaimSets { get; set; } + + public DbSet ClaimSetResourceClaimActions { get; set; } + + public DbSet ResourceClaims { get; set; } + + public DbSet ResourceClaimActions { get; set; } + + public DbSet ClaimSetResourceClaimActionAuthorizationStrategyOverrides { get; set; } + + public DbSet ResourceClaimActionAuthorizationStrategies { get; set; } + + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/SecurityContextFactory.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/SecurityContextFactory.cs new file mode 100644 index 000000000..c591d02ae --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/SecurityContextFactory.cs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common.Configuration; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Providers; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts +{ + public class SecurityContextFactory(ISecurityDatabaseConnectionStringProvider connectionStringProvider, DatabaseEngine databaseEngine) : ISecurityContextFactory + { + private readonly ISecurityDatabaseConnectionStringProvider _connectionStringProvider = connectionStringProvider; + private readonly DatabaseEngine _databaseEngine = databaseEngine; + private readonly IDictionary _securityContextTypeByDatabaseEngine = + new Dictionary + { + {DatabaseEngine.SqlServer, typeof(SqlServerSecurityContext)}, + {DatabaseEngine.Postgres, typeof(PostgresSecurityContext)} + }; + + public Type GetSecurityContextType() + { + if (_securityContextTypeByDatabaseEngine.TryGetValue(_databaseEngine, out Type? contextType)) + { + return contextType ?? throw new InvalidOperationException( + $"No SecurityContext defined for database type {_databaseEngine.DisplayName}"); + } + + throw new InvalidOperationException( + $"No SecurityContext defined for database type {_databaseEngine.DisplayName}"); + } + + public ISecurityContext CreateContext() + { + if (_databaseEngine == DatabaseEngine.SqlServer) + { + return (Activator.CreateInstance( + GetSecurityContextType(), + new DbContextOptionsBuilder() + .UseSqlServer(_connectionStringProvider.GetConnectionString()) + .Options) as ISecurityContext) + ?? throw new InvalidOperationException("Failed to create an instance of SqlServerSecurityContext."); + } + + if (_databaseEngine == DatabaseEngine.Postgres) + { + return (Activator.CreateInstance( + GetSecurityContextType(), + new DbContextOptionsBuilder() + .UseNpgsql(_connectionStringProvider.GetConnectionString()) + .UseLowerCaseNamingConvention() + .Options) as ISecurityContext) + ?? throw new InvalidOperationException("Failed to create an instance of PostgresSecurityContext."); + } + + throw new InvalidOperationException( + $"Cannot create a SecurityContext for database type {_databaseEngine.DisplayName}"); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/SqlServerSecurityContext.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/SqlServerSecurityContext.cs new file mode 100644 index 000000000..d29daa856 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Contexts/SqlServerSecurityContext.cs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts +{ + public class SqlServerSecurityContext : SecurityContext + { + // The default behavior is appropriate for this sub-class. + public SqlServerSecurityContext(DbContextOptions options) : base(options) { } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/Action.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/Action.cs new file mode 100644 index 000000000..651cbcc47 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/Action.cs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Models +{ + public class Action + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ActionId { get; set; } + + [StringLength(255)] + [Required] + public required string ActionName { get; set; } + + [StringLength(2048)] + [Required] + public required string ActionUri { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/Application.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/Application.cs new file mode 100644 index 000000000..bc9d92a49 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/Application.cs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Models +{ + public class Application + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ApplicationId { get; set; } + + public required string ApplicationName { get; set; } = string.Empty; + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/AuthorizationStrategy.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/AuthorizationStrategy.cs new file mode 100644 index 000000000..fee37af2b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/AuthorizationStrategy.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Models +{ + public class AuthorizationStrategy + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int AuthorizationStrategyId { get; set; } + + [StringLength(255)] + [Required] + public required string DisplayName { get; set; } + + [StringLength(255)] + [Required] + public required string AuthorizationStrategyName { get; set; } + + [Column("Application_ApplicationId")] + public int ApplicationId { get; set; } + + [Required] + public required Application Application { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ClaimSet.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ClaimSet.cs new file mode 100644 index 000000000..8cd522a60 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ClaimSet.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Models +{ + public class ClaimSet + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ClaimSetId { get; set; } + + [StringLength(255)] + [Required] + public required string ClaimSetName { get; set; } + + public bool IsEdfiPreset { get; set; } + + public bool ForApplicationUseOnly { get; set; } + + [Column("Application_ApplicationId")] + public int ApplicationId { get; set; } + + [Required] + public required Application Application { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ClaimSetResourceClaimAction.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ClaimSetResourceClaimAction.cs new file mode 100644 index 000000000..ff1ee928b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ClaimSetResourceClaimAction.cs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Models +{ + public class ClaimSetResourceClaimAction + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ClaimSetResourceClaimActionId { get; set; } + + public int ActionId { get; set; } + + [Required] + [ForeignKey("ActionId")] + public required Action Action { get; set; } + + public int ClaimSetId { get; set; } + + [Required] + [ForeignKey("ClaimSetId")] + public required ClaimSet ClaimSet { get; set; } + + public int ResourceClaimId { get; set; } + + [Required] + [ForeignKey("ResourceClaimId")] + public required ResourceClaim ResourceClaim { get; set; } + + public List? AuthorizationStrategyOverrides { get; set; } + + [StringLength(255)] + public string? ValidationRuleSetNameOverride { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ClaimSetResourceClaimActionAuthorizationStrategyOverrides.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ClaimSetResourceClaimActionAuthorizationStrategyOverrides.cs new file mode 100644 index 000000000..4e76c8498 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ClaimSetResourceClaimActionAuthorizationStrategyOverrides.cs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Models +{ + public class ClaimSetResourceClaimActionAuthorizationStrategyOverrides + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ClaimSetResourceClaimActionAuthorizationStrategyOverrideId { get; set; } + + public int ClaimSetResourceClaimActionId { get; set; } + + [Required] + [ForeignKey("ClaimSetResourceClaimActionId")] + public required ClaimSetResourceClaimAction ClaimSetResourceClaimAction { get; set; } + + public int AuthorizationStrategyId { get; set; } + + [Required] + [ForeignKey("AuthorizationStrategyId")] + public required AuthorizationStrategy AuthorizationStrategy { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ResourceClaim.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ResourceClaim.cs new file mode 100644 index 000000000..b54ced18e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ResourceClaim.cs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Models +{ + public class ResourceClaim + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ResourceClaimId { get; set; } + + [StringLength(255)] + [Required] + public required string DisplayName { get; set; } + + /// + /// ResourceName is actually an Uri so length needs to be around 2048 + /// + [StringLength(2048)] + [Required] + public required string ResourceName { get; set; } + + /// + /// ClaimName is actually an Uri so length needs to be around 2048 + /// + [StringLength(2048)] + [Required] + public required string ClaimName { get; set; } + + [Column("Application_ApplicationId")] + public int ApplicationId { get; set; } + + [Required] + public required Application Application { get; set; } + + public int? ParentResourceClaimId { get; set; } + + public virtual ResourceClaim? ParentResourceClaim { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ResourceClaimAction.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ResourceClaimAction.cs new file mode 100644 index 000000000..99081f01b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ResourceClaimAction.cs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Models +{ + public class ResourceClaimAction + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ResourceClaimActionId { get; set; } + + public int ActionId { get; set; } + + [Required] + [ForeignKey("ActionId")] + public required Action Action { get; set; } + + public List? AuthorizationStrategies { get; set; } + + public int ResourceClaimId { get; set; } + + [Required] + [ForeignKey("ResourceClaimId")] + public required ResourceClaim ResourceClaim { get; set; } + + [StringLength(255)] + public string? ValidationRuleSetName { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ResourceClaimActionAuthorizationStrategies.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ResourceClaimActionAuthorizationStrategies.cs new file mode 100644 index 000000000..19f22395d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Models/ResourceClaimActionAuthorizationStrategies.cs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Models +{ + public class ResourceClaimActionAuthorizationStrategies + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ResourceClaimActionAuthorizationStrategyId { get; set; } + + public int ResourceClaimActionId { get; set; } + + [Required] + [ForeignKey("ResourceClaimActionId")] + public required ResourceClaimAction ResourceClaimAction { get; set; } + + public int AuthorizationStrategyId { get; set; } + + [Required] + [ForeignKey("AuthorizationStrategyId")] + public required AuthorizationStrategy AuthorizationStrategy { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Providers/ISecurityDatabaseConnectionStringProvider.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Providers/ISecurityDatabaseConnectionStringProvider.cs new file mode 100644 index 000000000..c17f69a09 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Providers/ISecurityDatabaseConnectionStringProvider.cs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common.Database; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Providers +{ + public interface ISecurityDatabaseConnectionStringProvider : IDatabaseConnectionStringProvider { } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Providers/SecurityDatabaseConnectionStringProvider.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Providers/SecurityDatabaseConnectionStringProvider.cs new file mode 100644 index 000000000..5fa920290 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Providers/SecurityDatabaseConnectionStringProvider.cs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common.Configuration; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Providers { + public class SecurityDatabaseConnectionStringProvider: ISecurityDatabaseConnectionStringProvider + { + private readonly IConfigConnectionStringsProvider _configConnectionStringsProvider; + + public SecurityDatabaseConnectionStringProvider(IConfigConnectionStringsProvider configConnectionStringsProvider) + { + _configConnectionStringsProvider = configConnectionStringsProvider; + } + + public string GetConnectionString() => _configConnectionStringsProvider.GetConnectionString("EdFi_Security"); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Repositories/CachedSecurityRepository.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Repositories/CachedSecurityRepository.cs new file mode 100644 index 000000000..19c1d4d2e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Repositories/CachedSecurityRepository.cs @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using EdFi.Common.Extensions; +using EdFi.Common.Utils; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; +using Action = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Action; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Repositories +{ + public class CachedSecurityRepository : SecurityRepository + { + private readonly ReaderWriterLockSlim _cacheLock = new ReaderWriterLockSlim(); + private readonly int _cacheTimeoutInMinutes; + + private DateTime _lastCacheUpdate; + + public CachedSecurityRepository(ISecurityContextFactory securityContextFactory, int cacheTimeoutInMinutes) + : base(securityContextFactory) + { + if (cacheTimeoutInMinutes <= 0) + { + throw new ArgumentException("Cache Timeout in minutes must be a positive number.", nameof(cacheTimeoutInMinutes)); + } + + _cacheTimeoutInMinutes = cacheTimeoutInMinutes; + + // Current implementation of the base expects to initialize the data immediately, so reflect that with a cache update + _lastCacheUpdate = SystemClock.Now(); + } + + private bool ShouldUpdateCache + { + get => _lastCacheUpdate.IsDefaultValue() || SystemClock.Now() >= _lastCacheUpdate.AddMinutes(_cacheTimeoutInMinutes); + } + + public override Action? GetActionByHttpVerb(string httpVerb) + => VerifyCacheAndExecute(() => base.GetActionByHttpVerb(httpVerb)); + + public override Action? GetActionByName(string actionName) + => VerifyCacheAndExecute(() => base.GetActionByName(actionName)); + + public override AuthorizationStrategy GetAuthorizationStrategyByName(string authorizationStrategyName) + => VerifyCacheAndExecute(() => base.GetAuthorizationStrategyByName(authorizationStrategyName)); + + public override IEnumerable GetClaimsForClaimSet(string claimSetName) + => VerifyCacheAndExecute(() => base.GetClaimsForClaimSet(claimSetName)); + + public override IEnumerable GetResourceClaimLineage(string resourceUri) + => VerifyCacheAndExecute(() => base.GetResourceClaimLineage(resourceUri)); + + public override IEnumerable + GetResourceClaimLineageMetadata(string resourceClaimUri, string action) + => VerifyCacheAndExecute(() => base.GetResourceClaimLineageMetadata(resourceClaimUri, action)); + + public override ResourceClaim? GetResourceByResourceName(string resourceName) + => VerifyCacheAndExecute(() => base.GetResourceByResourceName(resourceName)); + + private T VerifyCacheAndExecute(Func executionFunction) + { + RefreshCacheIfNeeded(); + return executionFunction(); + } + + private void RefreshCacheIfNeeded() + { + if (!ShouldUpdateCache) + { + return; + } + + _cacheLock.EnterUpgradeableReadLock(); + + try + { + if (!ShouldUpdateCache) + { + return; + } + + _cacheLock.EnterWriteLock(); + + try + { + Reset(); + _lastCacheUpdate = SystemClock.Now(); + } + finally + { + _cacheLock.ExitWriteLock(); + } + } + finally + { + _cacheLock.ExitUpgradeableReadLock(); + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Repositories/ISecurityRepository.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Repositories/ISecurityRepository.cs new file mode 100644 index 000000000..e864ca8c5 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Repositories/ISecurityRepository.cs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; +using Action = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Action; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Repositories +{ + public interface ISecurityRepository + { + Action? GetActionByHttpVerb(string httpVerb); + + Action? GetActionByName(string actionName); + + AuthorizationStrategy GetAuthorizationStrategyByName(string authorizationStrategyName); + + IEnumerable GetClaimsForClaimSet(string claimSetName); + + /// + /// Gets the lineage up the taxonomy of resource claim URIs for the specified resource. + /// + /// The resource URI representing the resource. + /// The resource claim URIs. + IEnumerable GetResourceClaimLineage(string resourceUri); + + /// + /// Gets the authorization metadata of the lineage up the taxonomy of resource claims + /// for the specified resource. + /// + /// The resource claim URI representing the resource. + /// The resource claim authorization metadata. + IEnumerable GetResourceClaimLineageMetadata(string resourceClaimUri, string action); + + ResourceClaim? GetResourceByResourceName(string resourceName); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Repositories/SecurityRepository.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Repositories/SecurityRepository.cs new file mode 100644 index 000000000..482f989ed --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Repositories/SecurityRepository.cs @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Common; +using EdFi.Common.Utils.Extensions; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Contexts; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Repositories +{ + public class SecurityRepository : SecurityRepositoryBase, ISecurityRepository + { + private readonly ISecurityContextFactory _securityContextFactory; + + public SecurityRepository(ISecurityContextFactory securityContextFactory) + { + _securityContextFactory = Preconditions.ThrowIfNull(securityContextFactory, nameof(securityContextFactory)); + + Initialize( + GetApplication, + GetActions, + GetClaimSets, + GetResourceClaims, + GetAuthorizationStrategies, + GetClaimSetResourceClaimActions, + GetResourceClaimActionAuthorizations); + } + + private Application GetApplication() + { + using var context = _securityContextFactory.CreateContext(); + + return context.Applications.AsEnumerable().First( + app => app.ApplicationName.Equals("Ed-Fi ODS API", StringComparison.InvariantCultureIgnoreCase)); + } + + private List GetActions() + { + using var context = _securityContextFactory.CreateContext(); + + return [.. context.Actions]; + } + + private List GetClaimSets() + { + using var context = _securityContextFactory.CreateContext(); + + return [.. context.ClaimSets.Include(cs => cs.Application)]; + } + + private List GetResourceClaims() + { + using var context = _securityContextFactory.CreateContext(); + + return [.. context.ResourceClaims + .Include(rc => rc.Application) + .Include(rc => rc.ParentResourceClaim) + .Where(rc => rc.Application.ApplicationId.Equals(Application.Value.ApplicationId))]; + } + + private List GetAuthorizationStrategies() + { + using var context = _securityContextFactory.CreateContext(); + + return [.. context.AuthorizationStrategies + .Include(auth => auth.Application) + .Where(auth => auth.Application.ApplicationId.Equals(Application.Value.ApplicationId))]; + } + + private List GetClaimSetResourceClaimActions() + { + using var context = _securityContextFactory.CreateContext(); + + var claimSetResourceClaimActions = context.ClaimSetResourceClaimActions + .Include(csrc => csrc.Action) + .Include(csrc => csrc.ClaimSet) + .Include(csrc => csrc.ClaimSet.Application) + .Include(csrc => csrc.ResourceClaim) + .Include(csrc => csrc.AuthorizationStrategyOverrides!) + .ThenInclude(aso => aso.AuthorizationStrategy) + .Where(csrc => csrc.ResourceClaim.Application.ApplicationId.Equals(Application.Value.ApplicationId)) + .ToList(); + + // Replace empty lists with null since some consumers expect it that way + claimSetResourceClaimActions + .Where(csrc => csrc.AuthorizationStrategyOverrides != null && csrc.AuthorizationStrategyOverrides.Count == 0) + .ForEach(csrc => csrc.AuthorizationStrategyOverrides = null); + + return claimSetResourceClaimActions; + } + + private List GetResourceClaimActionAuthorizations() + { + using var context = _securityContextFactory.CreateContext(); + + var resourceClaimActionAuthorizationStrategies = context.ResourceClaimActionAuthorizationStrategies + .Include(rcaas => rcaas.AuthorizationStrategy) + .Include(rcaas => rcaas.ResourceClaimAction) + .ToList(); + + var resourceClaimActionAuthorizations = context.ResourceClaimActions + .Include(rcas => rcas.Action) + .Include(rcas => rcas.ResourceClaim) + .Include(rcas => rcas.AuthorizationStrategies!) + .ThenInclude(ast => ast.AuthorizationStrategy) + .Where(rcas => rcas.ResourceClaim.Application.ApplicationId.Equals(Application.Value.ApplicationId)) + .ToList(); + + foreach (var a in resourceClaimActionAuthorizations) + { + a.AuthorizationStrategies = resourceClaimActionAuthorizationStrategies.Where(r => r.ResourceClaimAction.ResourceClaimActionId == a.ResourceClaimActionId).ToList(); + } + + return resourceClaimActionAuthorizations; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Repositories/SecurityRepositoryBase.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Repositories/SecurityRepositoryBase.cs new file mode 100644 index 000000000..579ca4719 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Repositories/SecurityRepositoryBase.cs @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Models; +using EdFi.Ods.AdminApi.V1.Security.DataAccess.Utils; +using Action = EdFi.Ods.AdminApi.V1.Security.DataAccess.Models.Action; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Repositories +{ + public abstract class SecurityRepositoryBase + { + protected ResettableLazy Application { get; private set; } = default!; + + protected ResettableLazy> Actions { get; private set; } = default!; + + protected ResettableLazy> ClaimSets { get; private set; } = default!; + + protected ResettableLazy> ResourceClaims { get; private set; } = default!; + + protected ResettableLazy> AuthorizationStrategies { get; private set; } = default!; + + protected ResettableLazy> ClaimSetResourceClaimActions { get; private set; } = default!; + + protected ResettableLazy> ResourceClaimActions { get; private set; } = default!; + + protected void Initialize( + Func application, + Func> actions, + Func> claimSets, + Func> resourceClaims, + Func> authorizationStrategies, + Func> claimSetResourceClaimActions, + Func> resourceClaimActions) + { + Application = new ResettableLazy(application); + Actions = new ResettableLazy>(actions); + ClaimSets = new ResettableLazy>(claimSets); + ResourceClaims = new ResettableLazy>(resourceClaims); + AuthorizationStrategies = new ResettableLazy>(authorizationStrategies); + ClaimSetResourceClaimActions = new ResettableLazy>(claimSetResourceClaimActions); + ResourceClaimActions = new ResettableLazy>(resourceClaimActions); + } + + /// + /// Clears the cache, the database will be hit lazily. + /// + protected void Reset() + { + Application.Reset(); + Actions.Reset(); + ClaimSets.Reset(); + ResourceClaims.Reset(); + AuthorizationStrategies.Reset(); + ClaimSetResourceClaimActions.Reset(); + ResourceClaimActions.Reset(); + } + + public virtual Action? GetActionByHttpVerb(string httpVerb) + { + string actionName = string.Empty; + + switch (httpVerb) + { + case "GET": + actionName = "Read"; + break; + case "POST": + actionName = "Create"; + break; + case "PUT": + actionName = "Update"; + break; + case "DELETE": + actionName = "Delete"; + break; + } + + return GetActionByName(actionName); + } + + public virtual Action? GetActionByName(string actionName) + { + return Actions.Value.FirstOrDefault(a => a.ActionName.Equals(actionName, StringComparison.InvariantCultureIgnoreCase)); + } + + public virtual AuthorizationStrategy GetAuthorizationStrategyByName(string authorizationStrategyName) + { + return AuthorizationStrategies.Value.First( + a => a.AuthorizationStrategyName.Equals(authorizationStrategyName, StringComparison.InvariantCultureIgnoreCase)); + } + + public virtual IEnumerable GetClaimsForClaimSet(string claimSetName) + { + return ClaimSetResourceClaimActions.Value.AsEnumerable(). + Where(c => c.ClaimSet.ClaimSetName.Equals(claimSetName, StringComparison.InvariantCultureIgnoreCase)); + } + + /// + /// Gets the lineage up the taxonomy of resource claim URIs for the specified resource. + /// + /// The resource claim URI representing the resource. + /// The lineage of resource claim URIs. + public virtual IEnumerable GetResourceClaimLineage(string resourceClaimUri) + { + return GetResourceClaimLineageForResourceClaim(resourceClaimUri) + .Select(c => c.ClaimName); + } + + private IEnumerable GetResourceClaimLineageForResourceClaim(string resourceClaimUri) + { + var resourceClaimLineage = new List(); + + ResourceClaim? resourceClaim = null; + + try + { + resourceClaim = ResourceClaims.Value.AsEnumerable() + .SingleOrDefault(rc => rc.ClaimName.Equals(resourceClaimUri, StringComparison.InvariantCultureIgnoreCase)); + } + catch (InvalidOperationException ex) + { + // Use InvalidOperationException wrapper with custom message over InvalidOperationException + // thrown by Linq to communicate back to caller the problem with the configuration. + throw new InvalidOperationException($"Multiple resource claims with a claim name of '{resourceClaimUri}' were found in the Ed-Fi API's security configuration. Authorization cannot be performed.", ex); + } + + if (resourceClaim != null) + { + resourceClaimLineage.Add(resourceClaim); + + if (resourceClaim.ParentResourceClaim != null) + { + resourceClaimLineage.AddRange(GetResourceClaimLineageForResourceClaim(resourceClaim.ParentResourceClaim.ClaimName)); + } + } + + return resourceClaimLineage; + } + + /// + /// Gets the authorization metadata of the lineage up the resource claim taxonomy for the specified resource claim. + /// + /// The resource claim URI for which metadata is to be retrieved. + /// The resource claim's lineage of authorization metadata. + public virtual IEnumerable GetResourceClaimLineageMetadata(string resourceClaimUri, string action) + { + var strategies = new List(); + + AddStrategiesForResourceClaimLineage(strategies, resourceClaimUri, action); + + return strategies; + } + + private void AddStrategiesForResourceClaimLineage(List strategies, string resourceClaimUri, string action) + { + //check for exact match on resource and action + var claimAndStrategy = ResourceClaimActions + .Value + .SingleOrDefault( + rcas => + rcas.ResourceClaim.ClaimName.Equals(resourceClaimUri, StringComparison.InvariantCultureIgnoreCase) + && rcas.Action.ActionUri.Equals(action, StringComparison.InvariantCultureIgnoreCase)); + + // Add the claim/strategy if it was found + if (claimAndStrategy != null) + { + strategies.Add(claimAndStrategy); + } + + var resourceClaim = ResourceClaims + .Value + .FirstOrDefault(rc => rc.ClaimName.Equals(resourceClaimUri, StringComparison.InvariantCultureIgnoreCase)); + + // if there's a parent resource, recurse + if (resourceClaim != null && resourceClaim.ParentResourceClaim != null) + { + AddStrategiesForResourceClaimLineage(strategies, resourceClaim.ParentResourceClaim.ClaimName, action); + } + } + + public virtual ResourceClaim? GetResourceByResourceName(string resourceName) + { + return ResourceClaims + .Value + .FirstOrDefault(rc => rc.ResourceName.Equals(resourceName, StringComparison.InvariantCultureIgnoreCase)); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Utils/ResettableLazy.cs b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Utils/ResettableLazy.cs new file mode 100644 index 000000000..a0287597d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Security.DataAccess/Utils/ResettableLazy.cs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Threading; + +namespace EdFi.Ods.AdminApi.V1.Security.DataAccess.Utils +{ + public class ResettableLazy + { + private readonly Func _valueFactory; + private Lazy _lazy; + + public bool IsValueCreated => _lazy.IsValueCreated; + public T Value => _lazy.Value; + + public ResettableLazy(Func valueFactory) + { + _valueFactory = valueFactory; + _lazy = new Lazy(_valueFactory, LazyThreadSafetyMode.PublicationOnly); + } + + public void Reset() + { + _lazy = new Lazy(_valueFactory, LazyThreadSafetyMode.PublicationOnly); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Admin/00003-EditApiAuthTables.sql b/Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Admin/00003-EditApiAuthTables.sql deleted file mode 100644 index 90a1d92ad..000000000 --- a/Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Admin/00003-EditApiAuthTables.sql +++ /dev/null @@ -1,18 +0,0 @@ --- SPDX-License-Identifier: Apache-2.0 --- Licensed to the Ed-Fi Alliance under one or more agreements. --- The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. --- See the LICENSE and NOTICES files in the project root for more information. - -IF EXISTS (SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = 'adminapi' and TABLE_NAME = 'Applications') -BEGIN - ALTER TABLE adminapi.Applications - ADD - [ApplicationType] NVARCHAR(MAX) NULL, - [JsonWebKeySet] NVARCHAR(MAX) NULL, - [Settings] NVARCHAR(MAX) NULL; -END - -IF EXISTS (SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = 'adminapi' and TABLE_NAME = 'Applications') -BEGIN - EXEC sp_rename 'adminapi.Applications.Type', 'ClientType', 'COLUMN'; -END diff --git a/Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Security/00001-AdminConsoleClaimsetUp.sql b/Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Security/00001-AdminConsoleClaimsetUp.sql new file mode 100644 index 000000000..850616b1b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Security/00001-AdminConsoleClaimsetUp.sql @@ -0,0 +1,131 @@ +-- SPDX-License-Identifier: Apache-2.0 +-- Licensed to the Ed-Fi Alliance under one or more agreements. +-- The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +-- See the LICENSE and NOTICES files in the project root for more information. + +-- Create Ed-Fi ODS Admin Console ClaimSet + +DECLARE @claimSetName nvarchar(32) + +SET @claimSetName = 'Ed-Fi ODS Admin Console' + +PRINT 'Ensuring Ed-Fi ODS Admin Console Claimset exists.' + +INSERT INTO dbo.ClaimSets (ClaimSetName, IsEdfiPreset) +SELECT DISTINCT @claimSetName, 1 FROM dbo.ClaimSets +WHERE NOT EXISTS (SELECT 1 + FROM dbo.ClaimSets + WHERE ClaimSetName = @claimSetName ) +GO + +-- Configure Ed-Fi ODS Admin Console ClaimSet + +DECLARE @actionName nvarchar(32) +DECLARE @claimSetName nvarchar(255) +DECLARE @resourceNames TABLE (ResourceName nvarchar(64)) +DECLARE @resourceClaimIds TABLE (ResourceClaimId int) +DECLARE @authorizationStrategyId INT +DECLARE @ResourceClaimId INT + +SET @claimSetName = 'Ed-Fi ODS Admin Console' + +IF EXISTS (SELECT 1 FROM dbo.ClaimSets c WHERE c.ClaimSetName = @claimSetName) +BEGIN + DECLARE @edFiOdsAdminConsoleClaimSetId as INT + + SELECT @edFiOdsAdminConsoleClaimSetId = ClaimsetId + FROM dbo.ClaimSets + WHERE ClaimSets.ClaimSetName = @claimSetName + + DELETE csrcaaso + FROM dbo.ClaimSetResourceClaimActionAuthorizationStrategyOverrides csrcaaso + INNER JOIN dbo.ClaimSetResourceClaimActions ON csrcaaso.ClaimSetResourceClaimActionId = dbo.ClaimSetResourceClaimActions.ClaimSetResourceClaimActionId + WHERE dbo.ClaimSetResourceClaimActions.ClaimSetId = @edFiOdsAdminConsoleClaimSetId + + DELETE FROM dbo.ClaimSetResourceClaimActions + WHERE ClaimSetId = @edFiOdsAdminConsoleClaimSetId + + PRINT 'Creating Temporary Records.' + INSERT INTO @resourceNames VALUES + ('section'), + ('school'), + ('student'), + ('studentSchoolAssociation'), + ('studentSpecialEducationProgramAssociation'), + ('studentDisciplineIncidentBehaviorAssociation'), + ('studentSchoolAssociation'), + ('studentSchoolAttendanceEvent'), + ('studentSectionAssociation'), + ('staffEducationOrganizationAssignmentAssociation'), + ('staffSectionAssociation'), + ('courseTranscript') + INSERT INTO @resourceClaimIds SELECT ResourceClaimId FROM dbo.ResourceClaims WHERE ResourceName IN (SELECT ResourceName FROM @resourceNames) +END + +SELECT @authorizationStrategyId = AuthorizationStrategyId +FROM dbo.AuthorizationStrategies +WHERE AuthorizationStrategyName = 'NoFurtherAuthorizationRequired' + +DECLARE @actionId int +DECLARE @claimSetId int + +SELECT @claimSetId = ClaimSetId FROM dbo.ClaimSets WHERE ClaimSetName = @claimSetName + +PRINT 'Configuring Claims for Ed-Fi ODS Admin Console Claimset...' + +IF NOT EXISTS (SELECT 1 + FROM dbo.ClaimSetResourceClaimActions csraa,dbo.Actions a, @resourceClaimIds rc + WHERE csraa.ActionId = a.ActionId AND ClaimSetId = @claimSetId AND csraa.ResourceClaimId = rc.ResourceClaimId) + +BEGIN + INSERT INTO dbo.ClaimSetResourceClaimActions (ActionId, ClaimSetId, ResourceClaimId) + SELECT ActionId, @claimSetId, rc.ResourceClaimId + FROM dbo.Actions, @resourceClaimIds rc + WHERE ActionName in ('Read') + AND NOT EXISTS ( + SELECT 1 + FROM dbo.ClaimSetResourceClaimActions + WHERE ActionId = Actions.ActionId AND ClaimSetId = @claimSetId AND ResourceClaimId = rc.ResourceClaimId + ) + + INSERT INTO dbo.ClaimSetResourceClaimActionAuthorizationStrategyOverrides (AuthorizationStrategyId, ClaimSetResourceClaimActionId) + SELECT @authorizationStrategyId, ClaimSetResourceClaimActionId + FROM dbo.ClaimSetResourceClaimActions csrc + INNER JOIN dbo.ResourceClaims r + ON csrc.ResourceClaimId = r.ResourceClaimId AND csrc.ClaimSetId = @claimSetId + WHERE r.ResourceName IN ( + 'section', + 'school', + 'student', + 'studentSchoolAssociation', + 'studentSpecialEducationProgramAssociation', + 'studentDisciplineIncidentBehaviorAssociation', + 'studentSchoolAssociation', + 'studentSchoolAttendanceEvent', + 'studentSectionAssociation', + 'staffEducationOrganizationAssignmentAssociation', + 'staffSectionAssociation', + 'courseTranscript') +END + +SELECT @actionId = ActionId FROM dbo.Actions WHERE ActionName = 'Read' +SELECT @ResourceClaimId = ResourceClaimId FROM dbo.ResourceClaims WHERE ResourceName = 'types' + +IF NOT EXISTS ( + SELECT 1 FROM dbo.ClaimSetResourceClaimActions + WHERE ClaimSetResourceClaimActions.ActionId = @actionId AND ClaimSetResourceClaimActions.ClaimSetId = @claimSetId + AND ClaimSetResourceClaimActions.ResourceClaimId = @ResourceClaimId) +BEGIN + INSERT INTO dbo.ClaimSetResourceClaimActions (ActionId, ClaimSetId, ResourceClaimId) + VALUES (@actionId, @claimSetId, @ResourceClaimId) + + INSERT INTO dbo.ClaimSetResourceClaimActionAuthorizationStrategyOverrides (AuthorizationStrategyId, ClaimSetResourceClaimActionId) + SELECT @authorizationStrategyId, ClaimSetResourceClaimActions.ClaimSetResourceClaimActionId + FROM dbo.ClaimSetResourceClaimActions + INNER JOIN dbo.ResourceClaims r + ON ClaimSetResourceClaimActions.ResourceClaimId = r.ResourceClaimId + INNER JOIN dbo.Actions + ON Actions.actionId = ClaimSetResourceClaimActions.ActionId AND Actions.ActionName in ('Read') + WHERE r.ResourceName IN ('types') AND ClaimSetResourceClaimActions.ActionId = @actionId AND ClaimSetResourceClaimActions.ClaimSetId = @claimSetId +END +GO diff --git a/Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Admin/00003-EditApiAuthTables.sql b/Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Admin/00003-EditApiAuthTables.sql deleted file mode 100644 index 05484a5eb..000000000 --- a/Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Admin/00003-EditApiAuthTables.sql +++ /dev/null @@ -1,15 +0,0 @@ --- SPDX-License-Identifier: Apache-2.0 --- Licensed to the Ed-Fi Alliance under one or more agreements. --- The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. --- See the LICENSE and NOTICES files in the project root for more information. - -ALTER TABLE adminapi.Applications - ADD COLUMN - ApplicationType VARCHAR NULL, - ADD COLUMN - JsonWebKeySet VARCHAR NULL, - ADD COLUMN - Settings VARCHAR NULL; - -ALTER TABLE adminapi.Applications - RENAME COLUMN Type TO ClientType; diff --git a/Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Security/00001-AdminConsoleClaimsetUp.sql b/Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Security/00001-AdminConsoleClaimsetUp.sql new file mode 100644 index 000000000..affa89fc5 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Security/00001-AdminConsoleClaimsetUp.sql @@ -0,0 +1,122 @@ +-- SPDX-License-Identifier: Apache-2.0 +-- Licensed to the Ed-Fi Alliance under one or more agreements. +-- The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +-- See the LICENSE and NOTICES files in the project root for more information. + +-- Create Ed-Fi ODS Admin Console ClaimSet +DO $$ + DECLARE claimset_name varchar(50) := 'Ed-Fi ODS Admin Console'; + DECLARE claimset_id int; + DECLARE authorizationStrategy_id int; +BEGIN + + -- Creating Ed-Fi ODS Admin Console claim set + IF EXISTS (SELECT 1 FROM dbo.claimsets WHERE claimsetname = claimset_name) + THEN + RAISE NOTICE '% claimset exists', claimset_name; + ELSE + RAISE NOTICE 'adding % claimset', claimset_name; + INSERT INTO dbo.ClaimSets (ClaimSetName, isedfipreset) VALUES (claimset_name, True); + END IF; + +-- Configure Ed-Fi ODS Admin Console ClaimSet + + SELECT claimsetid INTO claimset_id + FROM dbo.claimsets + WHERE claimsetname = claimset_name; + + DELETE + FROM dbo.ClaimSetResourceClaimActionAuthorizationStrategyOverrides csrcaaso + USING dbo.ClaimSetResourceClaimActions csrc + WHERE csrcaaso.ClaimSetResourceClaimActionId = csrc.ClaimSetResourceClaimActionId AND csrc.ClaimSetId = claimset_id; + + DELETE FROM dbo.ClaimSetResourceClaimActions WHERE ClaimSetId = claimset_id; + + SELECT authorizationstrategyid INTO authorizationStrategy_id + FROM dbo.authorizationstrategies + WHERE authorizationstrategyname = 'NoFurtherAuthorizationRequired'; + + IF EXISTS (SELECT 1 FROM dbo.ClaimSetResourceClaimActions WHERE ClaimSetId = claimset_id) + THEN + RAISE NOTICE 'claims already exist for claim %', claimset_name; + ELSE + RAISE NOTICE 'Configuring Claims for % Claimset...', claimset_name; + INSERT INTO dbo.ClaimSetResourceClaimActions + (ActionId + ,ClaimSetId + ,ResourceClaimId) + SELECT ac.actionid, claimset_id, resourceclaimid + FROM dbo.resourceclaims + INNER JOIN LATERAL + ( + SELECT actionid + FROM dbo.actions + WHERE actionname in ('Read') + ) AS ac ON true + WHERE resourcename IN + ( + 'section', + 'school', + 'student', + 'studentSchoolAssociation', + 'studentSpecialEducationProgramAssociation', + 'studentDisciplineIncidentBehaviorAssociation', + 'studentSchoolAssociation', + 'studentSchoolAttendanceEvent', + 'studentSectionAssociation', + 'staffEducationOrganizationAssignmentAssociation', + 'staffSectionAssociation', + 'courseTranscript' + ); + + INSERT INTO dbo.ClaimSetResourceClaimActionAuthorizationStrategyOverrides + ( + AuthorizationStrategyId + ,ClaimSetResourceClaimActionId + ) + SELECT authorizationStrategy_id, csrc.ClaimSetResourceClaimActionId + FROM dbo.ClaimSetResourceClaimActions csrc + INNER JOIN dbo.ResourceClaims r + ON csrc.ResourceClaimId = r.ResourceClaimId AND csrc.ClaimSetId = claimset_id + WHERE r.resourcename IN + ( + 'section', + 'school', + 'student', + 'studentSchoolAssociation', + 'studentSpecialEducationProgramAssociation', + 'studentDisciplineIncidentBehaviorAssociation', + 'studentSchoolAssociation', + 'studentSchoolAttendanceEvent', + 'studentSectionAssociation', + 'staffEducationOrganizationAssignmentAssociation', + 'staffSectionAssociation', + 'courseTranscript' + ); + + END IF; + + INSERT INTO dbo.ClaimSetResourceClaimActions + (ActionId + ,ClaimSetId + ,ResourceClaimId) + SELECT ac.actionid, claimset_id, resourceclaimid + FROM dbo.resourceclaims + INNER JOIN LATERAL + ( + SELECT actionid + FROM dbo.actions + WHERE actionname in ('Read') + ) AS ac ON true + WHERE resourcename IN ('types'); + + INSERT INTO dbo.ClaimSetResourceClaimActionAuthorizationStrategyOverrides + (AuthorizationStrategyId + ,ClaimSetResourceClaimActionId) + SELECT authorizationStrategy_id, csrc.ClaimSetResourceClaimActionId + FROM dbo.ClaimSetResourceClaimActions csrc + INNER JOIN dbo.ResourceClaims r ON csrc.ResourceClaimId = r.ResourceClaimId + INNER JOIN dbo.Actions a ON a.ActionId = csrc.ActionId AND a.ActionName in ('Read') + WHERE resourcename IN ('types') AND csrc.ActionId = a.ActionId AND csrc.ClaimSetId = claimset_id; + +END $$; diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/.gitignore b/Application/EdFi.Ods.AdminApi/E2E Tests/.gitignore new file mode 100644 index 000000000..349471e33 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/.gitignore @@ -0,0 +1 @@ +!.automation_*.env diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/Admin API Docker.postman_environment.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API Docker.postman_environment.json similarity index 100% rename from Application/EdFi.Ods.AdminApi/E2E Tests/Admin API Docker.postman_environment.json rename to Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API Docker.postman_environment.json diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/Admin API E2E.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API E2E.postman_collection.json similarity index 81% rename from Application/EdFi.Ods.AdminApi/E2E Tests/Admin API E2E.postman_collection.json rename to Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API E2E.postman_collection.json index 15b0895fc..aa71790ce 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/Admin API E2E.postman_collection.json +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API E2E.postman_collection.json @@ -1,9 +1,9 @@ { "info": { - "_postman_id": "96235532-a9f8-4f99-adb6-4fb726cc1567", + "_postman_id": "47e794c4-64a5-47f8-99b9-49d8f0358bbe", "name": "Admin API E2E refactor", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "5260609" + "_exporter_id": "22794466" }, "item": [ { @@ -64,593 +64,63 @@ "type": "noauth" } }, - { - "name": "User Management", - "item": [ - { - "name": "Register", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is OK\", function () {\r", - " pm.response.to.have.status(200);\r", - "});\r", - "\r", - "const response = pm.response.json();\r", - "const result = pm.response.json().result;\r", - "\r", - "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", - " pm.expect(response).to.have.property(\"title\");\r", - "});\r", - "\r", - "pm.test(\"Response title is helpful and accurate\", function () {\r", - " pm.expect(response.title.toLowerCase()).to.contain(\"client\");\r", - " pm.expect(response.title.toLowerCase()).to.contain(\"registered\");\r", - "});\r", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "pm.collectionVariables.set(\"RegisteredClientId\", pm.variables.replaceIn('{{$guid}}'));\r", - "pm.collectionVariables.set(\"RegisteredClientSecret\", pm.variables.replaceIn('{{$guid}}'));\r", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "ClientId", - "value": "{{RegisteredClientId}}", - "type": "text" - }, - { - "key": "ClientSecret", - "value": "{{RegisteredClientSecret}}", - "type": "text" - }, - { - "key": "DisplayName", - "value": "Postman Test", - "type": "text" - } - ] - }, - "url": { - "raw": "{{API_URL}}/connect/register", - "host": [ - "{{API_URL}}" - ], - "path": [ - "connect", - "register" - ] - } - }, - "response": [] - }, - { - "name": "Register - Invalid", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is Bad Request\", function () {\r", - " pm.response.to.have.status(400);\r", - "});\r", - "\r", - "const response = pm.response.json();\r", - "\r", - "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", - " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response).to.have.property(\"errors\");\r", - "});\r", - "\r", - "pm.test(\"Response title is helpful and accurate\", function () {\r", - " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", - "});\r", - "\r", - "pm.test(\"Response errors include messages by property\", function () {\r", - " pm.expect(response.errors[\"ClientId\"].length).to.equal(1);\r", - " pm.expect(response.errors[\"ClientSecret\"].length).to.equal(1);\r", - " pm.expect(response.errors[\"DisplayName\"].length).to.equal(1);\r", - "});\r", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "ClientId", - "value": "", - "type": "text" - }, - { - "key": "ClientSecret", - "value": "", - "type": "text" - }, - { - "key": "DisplayName", - "value": "", - "type": "text" - } - ] - }, - "url": { - "raw": "{{API_URL}}/connect/register", - "host": [ - "{{API_URL}}" - ], - "path": [ - "connect", - "register" - ] - } - }, - "response": [] - }, - { - "name": "Register - Already Exists", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is Bad Request\", function () {\r", - " pm.response.to.have.status(400);\r", - "});\r", - "\r", - "const response = pm.response.json();\r", - "\r", - "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", - " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response).to.have.property(\"errors\");\r", - "});\r", - "\r", - "pm.test(\"Response title is helpful and accurate\", function () {\r", - " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", - "});\r", - "\r", - "pm.test(\"Response errors include messages by property\", function () {\r", - " pm.expect(response.errors.ClientId.length).to.equal(1);\r", - " pm.expect(response.errors.ClientId[0]).to.contain(\"already exists\");\r", - "});\r", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "ClientId", - "value": "{{RegisteredClientId}}", - "type": "text" - }, - { - "key": "ClientSecret", - "value": "{{RegisteredClientSecret}}", - "type": "text" - }, - { - "key": "DisplayName", - "value": "{{UserName}}", - "type": "text" - } - ] - }, - "url": { - "raw": "{{API_URL}}/connect/register", - "host": [ - "{{API_URL}}" - ], - "path": [ - "connect", - "register" - ] - } - }, - "response": [] - }, - { - "name": "Token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is OK\", function () {\r", - " pm.response.to.have.status(200);\r", - "});\r", - "\r", - "const response = pm.response.json();\r", - "\r", - "pm.test(\"Response includes token\", function () {\r", - " pm.expect(response).to.have.property(\"access_token\");\r", - " pm.expect(response).to.have.property(\"token_type\");\r", - " pm.expect(response).to.have.property(\"expires_in\");\r", - "\r", - " pm.expect(response[\"token_type\"]).to.equal(\"Bearer\");\r", - "});\r", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "client_id", - "value": "{{RegisteredClientId}}", - "type": "text" - }, - { - "key": "client_secret", - "value": "{{RegisteredClientSecret}}", - "type": "text" - }, - { - "key": "grant_type", - "value": "client_credentials", - "type": "text" - }, - { - "key": "scope", - "value": "edfi_admin_api/full_access", - "type": "text" - } - ] - }, - "url": { - "raw": "{{API_URL}}/connect/token", - "host": [ - "{{API_URL}}" - ], - "path": [ - "connect", - "token" - ] - } - }, - "response": [] - }, - { - "name": "Token - Invalid", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is Bad Request\", function () {\r", - " pm.response.to.have.status(400);\r", - "});\r", - "\r", - "const response = pm.response.json();\r", - "\r", - "pm.test(\"Response includes error message\", function () {\r", - " pm.expect(response).to.have.property(\"error\");\r", - " pm.expect(response).to.have.property(\"error_description\");\r", - " pm.expect(response).to.have.property(\"error_uri\");\r", - "});\r", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "client_id", - "value": "{{$guid}}", - "type": "text" - }, - { - "key": "client_secret", - "value": "", - "type": "text" - }, - { - "key": "grant_type", - "value": "client_credentials", - "type": "text" - } - ] - }, - "url": { - "raw": "{{API_URL}}/connect/token", - "host": [ - "{{API_URL}}" - ], - "path": [ - "connect", - "token" - ] - } - }, - "response": [] - }, - { - "name": "Token - Invalid Grant Type", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is Bad Request\", function () {\r", - " pm.response.to.have.status(400);\r", - "});\r", - "\r", - "const response = pm.response.json();\r", - "\r", - "pm.test(\"Response includes error message\", function () {\r", - " pm.expect(response).to.have.property(\"error\");\r", - " pm.expect(response).to.have.property(\"error_description\");\r", - " pm.expect(response).to.have.property(\"error_uri\");\r", - "\r", - " pm.expect(response[\"error_description\"]).to.contain(\"grant_type\");\r", - "});\r", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "client_id", - "value": "{{RegisteredClientId}}", - "type": "text" - }, - { - "key": "client_secret", - "value": "{{RegisteredClientSecret}}", - "type": "text" - }, - { - "key": "grant_type", - "value": "authorization_code", - "type": "text" - }, - { - "key": "scope", - "value": "edfi_admin_api/full_access", - "type": "text" - } - ] - }, - "url": { - "raw": "{{API_URL}}/connect/token", - "host": [ - "{{API_URL}}" - ], - "path": [ - "connect", - "token" - ] - } - }, - "response": [] - }, - { - "name": "Token - Invalid Scope", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is Bad Request\", function () {\r", - " pm.response.to.have.status(400);\r", - "});\r", - "\r", - "const response = pm.response.json();\r", - "\r", - "pm.test(\"Response includes error message\", function () {\r", - " pm.expect(response).to.have.property(\"error\");\r", - " pm.expect(response).to.have.property(\"error_description\");\r", - " pm.expect(response).to.have.property(\"error_uri\");\r", - "\r", - " pm.expect(response[\"error_description\"]).to.contain(\"scope\");\r", - "});\r", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "client_id", - "value": "{{RegisteredClientId}}", - "type": "text" - }, - { - "key": "client_secret", - "value": "{{$guid}}", - "type": "text" - }, - { - "key": "grant_type", - "value": "client_credentials", - "type": "text" - }, - { - "key": "scope", - "value": "NOT_REAL/SCOPE", - "type": "text" - } - ] - }, - "url": { - "raw": "{{API_URL}}/connect/token", - "host": [ - "{{API_URL}}" - ], - "path": [ - "connect", - "token" - ] - } - }, - "response": [] - }, - { - "name": "Token - Incorrect Secret", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is Unauthorized\", function () {\r", - " pm.response.to.have.status(401);\r", - "});\r", - "\r", - "const response = pm.response.json();\r", - "\r", - "pm.test(\"Response includes error message\", function () {\r", - " pm.expect(response).to.have.property(\"error\");\r", - " pm.expect(response).to.have.property(\"error_description\");\r", - " pm.expect(response).to.have.property(\"error_uri\");\r", - "\r", - " pm.expect(response[\"error_description\"]).to.contain(\"credentials\");\r", - " pm.expect(response[\"error_description\"]).to.contain(\"invalid\");\r", - "});\r", - "\r", - "pm.collectionVariables.unset(\"RegisteredClientId\");\r", - "pm.collectionVariables.unset(\"RegisteredClientSecret\");" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "client_id", - "value": "{{RegisteredClientId}}", - "type": "text" - }, - { - "key": "client_secret", - "value": "{{$guid}}", - "type": "text" - }, - { - "key": "grant_type", - "value": "client_credentials", - "type": "text" - }, - { - "key": "scope", - "value": "edfi_admin_api/full_access", - "type": "text" - } - ] - }, - "url": { - "raw": "{{API_URL}}/connect/token", - "host": [ - "{{API_URL}}" - ], - "path": [ - "connect", - "token" - ] - } - }, - "response": [] - } - ], - "auth": { - "type": "noauth" - } - }, { "name": "v1", "item": [ { "name": "Vendors", "item": [ + { + "name": "Vendors - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Test Company\",\r\n \"namespacePrefixes\": \"uri://ed-fi.org\",\r\n \"contactName\": \"Test User\",\r\n \"contactEmailAddress\": \"test@test-ed-fi.org\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ] + } + }, + "response": [] + }, { "name": "Vendors", "event": [ @@ -692,7 +162,8 @@ " pm.collectionVariables.set(\"CreatedVendorId\", result.vendorId);\r", "}" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -788,7 +259,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -804,7 +274,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -900,6 +371,60 @@ }, "response": [] }, + { + "name": "Vendors - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ] + } + }, + "response": [] + }, { "name": "Vendors by ID", "event": [ @@ -1024,6 +549,68 @@ }, "response": [] }, + { + "name": "Vendors - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Updated Test Company\",\r\n \"namespacePrefixes\": \"uri://academicbenchmarks.com\",\r\n \"contactName\": \"Updated User\",\r\n \"contactEmailAddress\": \"updated@example.com\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, { "name": "Vendors - Invalid", "event": [ @@ -1038,7 +625,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -1054,7 +640,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -1063,7 +650,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -1093,6 +681,49 @@ }, "response": [] }, + { + "name": "Vendors - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, { "name": "Vendors", "event": [ @@ -1152,9 +783,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -1166,7 +795,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -1201,9 +831,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -1215,7 +843,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -1259,9 +888,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -1275,7 +902,8 @@ "pm.collectionVariables.unset(\"CreatedVendorId\");\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -1296,11 +924,157 @@ }, "response": [] } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } ] }, { "name": "Application", "item": [ + { + "name": "Applications - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v1/vendors`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Application Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Application User\",\r", + " \"contactEmailAddress\": \"application@example.com\"\r", + " }), \r", + " }\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json.result.vendorId) { console.log('Error in Pre-request: vendorID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ApplicationVendorId\", json.result.vendorId);\r", + " }\r", + "});\r", + "\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v1/odsInstances`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"name\": \"my osd instance test\",\r", + " \"instanceType\": \"test type\",\r", + " \"status\": \"test status\",\r", + " \"version\": \"test version\"\r", + " }), \r", + " }\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json.result.id) { console.log('Error in Pre-request: id missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"OdsInstanceId\", json.result.id);\r", + " }\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileId\": null,\r\n \"odsInstanceId\": {{OdsInstanceId}},\r\n \"educationOrganizationIds\": [ 255901 ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, { "name": "Applications", "event": [ @@ -1444,7 +1218,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -1458,7 +1233,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -1474,7 +1248,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -1513,7 +1288,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -1527,7 +1303,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -1542,7 +1317,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -1581,7 +1357,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -1596,7 +1373,6 @@ "const errors = pm.response.json().errors;\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -1611,7 +1387,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -1711,6 +1488,49 @@ }, "response": [] }, + { + "name": "Applications - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, { "name": "Applications by ID", "event": [ @@ -2019,6 +1839,68 @@ }, "response": [] }, + { + "name": "Applications - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationId\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Updated Application Name\",\r\n \"vendorId\": {{OtherApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi ODS Admin App\",\r\n \"profileId\": null,\r\n \"educationOrganizationIds\": [1234]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, { "name": "Applications - Invalid", "event": [ @@ -2028,7 +1910,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -2042,7 +1925,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -2058,7 +1940,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2097,7 +1980,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -2111,7 +1995,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -2126,7 +2009,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2165,7 +2049,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -2180,7 +2065,6 @@ "const errors = pm.response.json().errors;\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -2195,7 +2079,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2279,6 +2164,93 @@ }, "response": [] }, + { + "name": "Reset Credential - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}", + "reset-credential" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, { "name": "Applications", "event": [ @@ -2337,9 +2309,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -2351,7 +2321,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2386,9 +2357,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -2400,7 +2369,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2436,9 +2406,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -2450,7 +2418,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2494,9 +2463,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -2562,11 +2529,105 @@ }, "response": [] } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } ] }, { "name": "ClaimSets", "item": [ + { + "name": "ClaimSets - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"ClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test ClaimSet {{ClaimSetGUID}}\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"create\": true,\r\n \"read\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"readChanges\": true,\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningObjective\",\r\n \"create\": true,\r\n \"read\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"readChanges\": true,\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"create\": true,\r\n \"read\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"readChanges\": true,\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, { "name": "ClaimSets", "event": [ @@ -2654,7 +2715,8 @@ "exec": [ "pm.collectionVariables.set(\"ClaimSetOverrideGUID\", pm.variables.replaceIn('{{$guid}}'));" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -2669,7 +2731,6 @@ "const result = pm.response.json().result;\r", "\r", "pm.test(\"Override Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(201);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -2693,7 +2754,8 @@ "}\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2732,7 +2794,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -2746,7 +2809,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -2760,7 +2822,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2799,7 +2862,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -2813,7 +2877,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -2827,7 +2890,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2888,7 +2952,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -2902,7 +2967,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -2917,7 +2981,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2956,7 +3021,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -2971,7 +3037,6 @@ "const errors = pm.response.json().errors;\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -2994,7 +3059,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -3033,7 +3099,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -3048,7 +3115,6 @@ "const errors = pm.response.json().errors;\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -3068,7 +3134,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -3107,7 +3174,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -3122,7 +3190,6 @@ "const errors = pm.response.json().errors;\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -3138,7 +3205,8 @@ " });\r", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -3177,7 +3245,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -3192,7 +3261,6 @@ "const errors = pm.response.json().errors;\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -3209,7 +3277,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -3309,12 +3378,55 @@ "method": "GET", "header": [], "url": { - "raw": "{{API_URL}}/v1/claimsets/", + "raw": "{{API_URL}}/v1/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/", "host": [ "{{API_URL}}" ], "path": [ - "v1", + "v2", "claimsets", "" ] @@ -3393,7 +3505,6 @@ "const result = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -3416,7 +3527,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -3446,7 +3558,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -3461,7 +3574,6 @@ "const result = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -3483,7 +3595,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -3513,6 +3626,68 @@ }, "response": [] }, + { + "name": "ClaimSets - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"Updated Test ClaimSet\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"readChanges\": true,\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningObjective\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"readChanges\": true,\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n } \r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, { "name": "ClaimSets - Override", "event": [ @@ -3522,7 +3697,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -3537,7 +3713,6 @@ "const result = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -3565,7 +3740,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -3604,7 +3780,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -3618,7 +3795,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -3631,7 +3807,8 @@ " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -3670,7 +3847,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -3684,7 +3862,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -3697,7 +3874,8 @@ " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -3736,7 +3914,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -3750,7 +3929,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -3764,7 +3942,8 @@ " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -3803,7 +3982,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -3818,7 +3998,6 @@ "const errors = pm.response.json().errors;\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -3837,7 +4016,8 @@ " });\r", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -3876,7 +4056,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -3891,7 +4072,6 @@ "const errors = pm.response.json().errors;\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -3910,7 +4090,8 @@ " });\r", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -3949,7 +4130,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -3964,7 +4146,6 @@ "const errors = pm.response.json().errors;\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -3980,7 +4161,8 @@ " });\r", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -4019,7 +4201,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -4034,7 +4217,6 @@ "const errors = pm.response.json().errors;\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -4050,7 +4232,8 @@ " });\r", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -4089,7 +4272,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -4103,7 +4287,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -4119,7 +4302,8 @@ " });\r", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -4149,6 +4333,49 @@ }, "response": [] }, + { + "name": "ClaimSets - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, { "name": "ClaimSets", "event": [ @@ -4163,7 +4390,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", "});\r", "\r", @@ -4173,7 +4399,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -4208,7 +4435,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", "});\r", "\r", @@ -4218,7 +4444,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -4253,7 +4480,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -4272,7 +4498,8 @@ "pm.collectionVariables.unset(\"SystemReservedClaimSetId\");\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -4281,7 +4508,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -4316,7 +4544,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -4396,9 +4623,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -4410,7 +4635,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -4445,9 +4671,7 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -4455,7 +4679,8 @@ "});\r", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -4499,9 +4724,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -4549,11 +4772,104 @@ }, "response": [] } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } ] }, { "name": "OdsInstances", "item": [ + { + "name": "OdsInstances - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test Ods Instance\",\r\n \"instanceType\": \"Test Type\",\r\n \"status\": \"Test Status\",\r\n \"isExtended\": true,\r\n \"version\": \"Test Version\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ] + } + }, + "response": [] + }, { "name": "OdsInstances", "event": [ @@ -4569,7 +4885,6 @@ "const result = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(201);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -4650,7 +4965,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -4721,7 +5035,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches error format\", function () {\r", - " pm.expect(response.status).to.equal(400);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"errors\");\r", "});\r", @@ -4791,7 +5104,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -4855,6 +5167,70 @@ }, "response": [] }, + { + "name": "OdsInstance - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ] + } + }, + "response": [] + }, { "name": "OdsInstance by ID", "event": [ @@ -4870,7 +5246,6 @@ "const result = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -4924,7 +5299,6 @@ "const result = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -4984,6 +5358,68 @@ }, "response": [] }, + { + "name": "OdsInstances - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Updated Ods Instance\",\r\n \"instanceType\": \"Updated Type\",\r\n \"status\": \"Updated Status\",\r\n \"isExtended\": false,\r\n \"version\": \"Updated Version\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{CreateOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{CreateOdsInstanceId}}" + ] + } + }, + "response": [] + }, { "name": "OdsInstances Not allowed", "event": [ @@ -4998,9 +5434,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(400);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -5109,6 +5543,49 @@ }, "response": [] }, + { + "name": "OdsInstances - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{CreateOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{CreateOdsInstanceId}}" + ] + } + }, + "response": [] + }, { "name": "OdsInstances", "event": [ @@ -5123,7 +5600,6 @@ "const response = pm.response.json();\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", "});\r", "\r", @@ -5169,9 +5645,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -5219,9 +5693,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -5278,9 +5750,7 @@ "pm.test(\"Response matches error format\", function () {\r", " const response = pm.response.json();\r", "\r", - " pm.expect(response).to.have.property(\"status\");\r", " pm.expect(response).to.have.property(\"title\");\r", - " pm.expect(response.status).to.equal(404);\r", "});\r", "\r", "pm.test(\"Response title is helpful and accurate\", function () {\r", @@ -5316,27 +5786,246 @@ }, "response": [] } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "Tenants", + "item": [ + { + "name": "Tenants", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Tenants: Status code is Found\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "const result = pm.response.json();", + "", + "pm.test(\"GET Tenant Name: Response result matches tenant\", function () {", + " pm.expect(result.tenantName).to.equal(\"default\");", + " pm.expect(result.adminConnectionString.host).to.not.equal(null);", + " pm.expect(result.adminConnectionString.database).to.not.equal(null);", + " pm.expect(result.securityConnectionString.host).to.not.equal(null);", + " pm.expect(result.securityConnectionString.database).to.not.equal(null);", + "});", + "", + "const GetTenantsSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"tenantName\": {", + " \"type\": \"string\"", + " },", + " \"adminConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " },", + " },", + " \"securityConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " }", + " }", + " },", + " \"required\": [", + " \"tenantName\"", + " ]", + "}", + "", + "pm.test(\"GET Tenants: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetTenantsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "tenants" + ] + } + }, + "response": [] + }, + { + "name": "Tenants - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + "});", + "", + "pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants" + ] + } + }, + "response": [] + } ] } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } ] } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{TOKEN}}", - "type": "string" - } - ] - }, "event": [ { "listen": "prerequest", "script": { "type": "text/javascript", "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", "if(pm.request.auth && pm.request.auth.type === \"noauth\") {", " return;", "}", @@ -5346,8 +6035,11 @@ " return;", "}", "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", "pm.sendRequest({", - " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", " method: 'POST',", " header: {", " 'Content-Type': 'application/x-www-form-urlencoded'", @@ -5355,10 +6047,9 @@ " body: {", " mode: 'urlencoded',", " urlencoded: [", - " {key: 'client_id', value: pm.variables.get(\"RegisteredClientId\")},", - " {key: 'client_secret', value: pm.variables.get(\"RegisteredClientSecret\")},", - " {key: 'grant_type', value: \"client_credentials\"},", - " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", " ]", " }", "},", @@ -5367,8 +6058,31 @@ " if(error) {", " throw res.json().error_description", " }", - " pm.collectionVariables.set(\"TOKEN\", res.json().access_token);", - " console.log(\"Token success\");", + "", + " pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + " },", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid},", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + " },", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token);", + " console.log(\"Token success\");", + " });", "});" ] } diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/Admin API E2E.postman_collection_offset_and_limit.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API E2E.postman_collection_offset_and_limit.json similarity index 94% rename from Application/EdFi.Ods.AdminApi/E2E Tests/Admin API E2E.postman_collection_offset_and_limit.json rename to Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API E2E.postman_collection_offset_and_limit.json index f67a4d435..15e625aff 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/Admin API E2E.postman_collection_offset_and_limit.json +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API E2E.postman_collection_offset_and_limit.json @@ -1,9 +1,9 @@ { "info": { - "_postman_id": "33f67ba7-2151-40b5-ae76-7fe792a3e91a", + "_postman_id": "034b80cf-a2d4-478b-9b91-bcb12ec5cb38", "name": "Admin API E2E refactor Offset and Limit", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "5260609" + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", + "_exporter_id": "22794466" }, "item": [ { @@ -27,7 +27,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -101,7 +100,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -175,7 +173,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -278,6 +275,7 @@ " vendorsToDelete.push(vendorId);\r", " });\r", "}\r", + "\r", "pm.collectionVariables.set(\"VENDORSTODELETE\", vendorsToDelete);" ] } @@ -288,7 +286,7 @@ "type": "text/javascript", "packages": {}, "exec": [ - "let vendorsToDelete = pm.collectionVariables.get(\"VENDORSTODELETE\").split(\",\");\r", + "let vendorsToDelete = pm.collectionVariables.get(\"VENDORSTODELETE\");\r", "for (let i = 0; i < vendorsToDelete.length; i++) {\r", " let id = vendorsToDelete[i];\r", " pm.sendRequest({ \r", @@ -328,7 +326,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -380,7 +377,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -432,7 +428,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -595,7 +590,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -658,7 +652,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -710,7 +703,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -831,7 +823,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -905,7 +896,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -979,7 +969,6 @@ "const results = pm.response.json().result;\r", "\r", "pm.test(\"Response matches success format\", function () {\r", - " pm.expect(response.status).to.equal(200);\r", " pm.expect(response).to.have.property(\"title\");\r", " pm.expect(response).to.have.property(\"result\");\r", "});\r", @@ -1082,7 +1071,7 @@ " odsInstancesToDelete.push(odsInstanceId);\r", " });\r", "}\r", - "pm.collectionVariables.set(\"ODSINSTANCESTODELETE\", odsInstancesToDelete);" + "pm.collectionVariables.set(\"ODSINSTANCESTODELETE\", odsInstancesToDelete.join(','));" ] } }, @@ -1119,13 +1108,9 @@ ], "auth": { "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{TOKEN}}", - "type": "string" - } - ] + "bearer": { + "token": "{{TOKEN}}" + } }, "event": [ { @@ -1133,12 +1118,47 @@ "script": { "type": "text/javascript", "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", "let currentToken = pm.collectionVariables.get(\"TOKEN\");", "if(currentToken) {", " return;", "}", "", "var guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", "var header = {", " 'Content-Type': 'application/x-www-form-urlencoded'", "};", @@ -1151,7 +1171,7 @@ " mode: 'urlencoded',", " urlencoded: [", " {key: 'ClientId', value: guid },", - " {key: 'ClientSecret', value: guid },", + " {key: 'ClientSecret', value: client_secret },", " {key: 'DisplayName', value: guid }", " ]", " }", @@ -1165,7 +1185,7 @@ " mode: 'urlencoded',", " urlencoded: [", " {key: 'client_id', value: guid },", - " {key: 'client_secret', value: guid },", + " {key: 'client_secret', value: client_secret },", " {key: 'grant_type', value: \"client_credentials\"},", " {key: 'scope', value: \"edfi_admin_api/full_access\"}", " ]", @@ -1211,43 +1231,35 @@ }, { "key": "VENDORSTODELETE", - "value": "", - "type": "string" + "value": "" }, { "key": "VENDORSCOUNT", - "value": "10", - "type": "string" + "value": "10" }, { "key": "APPLICATIONSTODELETE", - "value": "", - "type": "string" + "value": "" }, { "key": "APPLICATIONCOUNT", - "value": "10", - "type": "string" + "value": "10" }, { "key": "CLAIMSETSTODELETE", - "value": "", - "type": "string" + "value": "" }, { "key": "CLAIMSETCOUNT", - "value": "10", - "type": "string" + "value": "10" }, { "key": "ODSINSTANCESTODELETE", - "value": "", - "type": "string" + "value": "" }, { "key": "ODSINSTANCESCOUNT", - "value": "10", - "type": "string" + "value": "10" } ] } \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/Admin API.postman_environment.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API.postman_environment.json similarity index 100% rename from Application/EdFi.Ods.AdminApi/E2E Tests/Admin API.postman_environment.json rename to Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API.postman_environment.json diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/README.md b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/README.md similarity index 100% rename from Application/EdFi.Ods.AdminApi/E2E Tests/README.md rename to Application/EdFi.Ods.AdminApi/E2E Tests/V1/README.md diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/gh-action-setup/.automation.env b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/gh-action-setup/.automation.env similarity index 55% rename from Application/EdFi.Ods.AdminApi/E2E Tests/gh-action-setup/.automation.env rename to Application/EdFi.Ods.AdminApi/E2E Tests/V1/gh-action-setup/.automation.env index 329d0ea18..d4fc92f68 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/gh-action-setup/.automation.env +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/gh-action-setup/.automation.env @@ -4,6 +4,7 @@ ODS_VIRTUAL_NAME=api ADMIN_API_VIRTUAL_NAME=adminapi LOGS_FOLDER=/tmp/logs ODS_API_VERSION=6.1 +ADMINAPI_MODE=V1 # For Authentication AUTHORITY=http://localhost/${ADMIN_API_VIRTUAL_NAME} @@ -19,7 +20,18 @@ PGBOUNCER_LISTEN_PORT=6432 SQLSERVER_USER=edfi SQLSERVER_PASSWORD=P@55w0rd -ADMIN_API_HEALTHCHECK_TEST="curl -f http://${ADMIN_API_VIRTUAL_NAME}/health" +ADMIN_API_HEALTHCHECK_TEST="wget -nv -t1 --spider http://${ADMIN_API_VIRTUAL_NAME}/health || exit 1" + +PAGING_OFFSET=0 +PAGING_LIMIT=25 # Ods Api API_HEALTHCHECK_TEST="curl -f http://localhost/health" + +IPRATELIMITING__ENABLEENDPOINTRATELIMITING=false +IPRATELIMITING__STACKBLOCKEDREQUESTS=false +IPRATELIMITING__REALIPHEADER=X-Real-IP +IPRATELIMITING__CLIENTIDHEADER=X-ClientId +IPRATELIMITING__HTTPSTATUSCODE=429 +IPRATELIMITING__IPWHITELIST=[] +IPRATELIMITING__ENDPOINTWHITELIST=[] \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/gh-action-setup/inspect.sh b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/gh-action-setup/inspect.sh similarity index 82% rename from Application/EdFi.Ods.AdminApi/E2E Tests/gh-action-setup/inspect.sh rename to Application/EdFi.Ods.AdminApi/E2E Tests/V1/gh-action-setup/inspect.sh index 6156e79a3..3624ec75b 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/gh-action-setup/inspect.sh +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/gh-action-setup/inspect.sh @@ -15,7 +15,7 @@ else exit 1 fi -status=`curl -k --silent --output /dev/null -w '%{http_code}' https://localhost/adminapi` +status=`wget -nv -t1 --spider -S https://localhost/adminapi/health --no-check-certificate 2>&1|grep "HTTP/"|awk '{print $2}'` if [[ $status -eq "200" ]] then diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-mssql.postman_environment.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-mssql.postman_environment.json new file mode 100644 index 000000000..811c6f7b8 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-mssql.postman_environment.json @@ -0,0 +1,69 @@ +{ + "id": "dbcd108a-4fc9-4266-9277-489c5c245e14", + "name": "Admin API Docker", + "values": [ + { + "key": "API_URL", + "value": "https://localhost/adminapi", + "type": "default", + "enabled": true + }, + { + "key": "offset", + "value": "0", + "type": "default", + "enabled": true + }, + { + "key": "limit", + "value": "100", + "type": "default", + "enabled": true + }, + { + "key": "connectionString", + "value": "Data Source=db-admin;Initial Catalog=EdFi_Admin;User Id=edfi;Password=P@55w0rd;Encrypt=false;TrustServerCertificate=true", + "type": "default", + "enabled": true + }, + { + "key": "securityconnectionString", + "value": "Data Source=db-admin;Initial Catalog=EdFi_Security;User Id=edfi;Password=P@55w0rd;Encrypt=false;TrustServerCertificate=true", + "type": "default", + "enabled": true + }, + { + "key": "isMultitenant", + "value": "true", + "type": "default", + "enabled": true + }, + { + "key": "tenant1", + "value": "Tenant1", + "type": "default", + "enabled": true + }, + { + "key": "tenant2", + "value": "Tenant2", + "type": "default", + "enabled": true + }, + { + "key": "sortByProperty", + "value": "function sortByProperty(objArray, prop, direction){ if (arguments.length<2) throw new Error(\"ARRAY, AND OBJECT PROPERTY MINIMUM ARGUMENTS, OPTIONAL DIRECTION\"); if (!Array.isArray(objArray)) throw new Error(\"FIRST ARGUMENT NOT AN ARRAY\"); const clone = objArray.slice(0); const direct = arguments.length>2 ? arguments[2] : 1; const propPath = (prop.constructor===Array) ? prop : prop.split(\".\"); clone.sort(function(a,b){ for (let p in propPath){ if (a[propPath[p]] && b[propPath[p]]){ a = a[propPath[p]]; b = b[propPath[p]]; } } a = a.match(/^\\d+$/) ? +a : a; b = b.match(/^\\d+$/) ? +b : b; return ( (a < b) ? -1*direct : ((a > b) ? 1*direct : 0) ); }); return clone; }", + "type": "default", + "enabled": true + }, + { + "key": "jsHelper", + "value": "(function () { const rndChar = function (str) { return str.charAt(Math.floor(Math.random() * str.length)); }; const shfChar = function (str) { const array = str.split(''); for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array.join(''); }; return { generateClientSecret: function(){ const minLength = 32; const maxLength = 128; let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~'; const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength; result += rndChar('abcdefghijklmnopqrstuvwxyz'); result += rndChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ'); result += rndChar('0123456789'); result += rndChar(specialCharacters); for (let i = result.length; i < length; i++) { const charactersPlusSpecial = characters + specialCharacters; result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length)); } return shfChar(result); }, randomChar : rndChar, shuffleString: shfChar } })", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2023-08-11T19:57:02.536Z", + "_postman_exported_using": "Postman/10.17.0" +} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-pgsql.postman_environment.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-pgsql.postman_environment.json new file mode 100644 index 000000000..723d046e6 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-pgsql.postman_environment.json @@ -0,0 +1,69 @@ +{ + "id": "dbcd108a-4fc9-4266-9277-489c5c245e14", + "name": "Admin API Docker", + "values": [ + { + "key": "API_URL", + "value": "https://localhost/adminapi", + "type": "default", + "enabled": true + }, + { + "key": "offset", + "value": "0", + "type": "default", + "enabled": true + }, + { + "key": "limit", + "value": "100", + "type": "default", + "enabled": true + }, + { + "key": "connectionString", + "value": "host=test;port=90;username=test;password=test;database=EdFi_Admin;pooling=false", + "type": "default", + "enabled": true + }, + { + "key": "securityconnectionString", + "value": "host=test;port=90;username=test;password=test;database=EdFi_Security;pooling=false", + "type": "default", + "enabled": true + }, + { + "key": "isMultitenant", + "value": "true", + "type": "default", + "enabled": true + }, + { + "key": "tenant1", + "value": "Tenant1", + "type": "default", + "enabled": true + }, + { + "key": "tenant2", + "value": "Tenant2", + "type": "default", + "enabled": true + }, + { + "key": "sortByProperty", + "value": "function sortByProperty(objArray, prop, direction){ if (arguments.length<2) throw new Error(\"ARRAY, AND OBJECT PROPERTY MINIMUM ARGUMENTS, OPTIONAL DIRECTION\"); if (!Array.isArray(objArray)) throw new Error(\"FIRST ARGUMENT NOT AN ARRAY\"); const clone = objArray.slice(0); const direct = arguments.length>2 ? arguments[2] : 1; const propPath = (prop.constructor===Array) ? prop : prop.split(\".\"); clone.sort(function(a,b){ for (let p in propPath){ if (a[propPath[p]] && b[propPath[p]]){ a = a[propPath[p]]; b = b[propPath[p]]; } } a = a.match(/^\\d+$/) ? +a : a; b = b.match(/^\\d+$/) ? +b : b; return ( (a < b) ? -1*direct : ((a > b) ? 1*direct : 0) ); }); return clone; }", + "type": "default", + "enabled": true + }, + { + "key": "jsHelper", + "value": "(function () { const rndChar = function (str) { return str.charAt(Math.floor(Math.random() * str.length)); }; const shfChar = function (str) { const array = str.split(''); for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array.join(''); }; return { generateClientSecret: function(){ const minLength = 32; const maxLength = 128; let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~'; const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength; result += rndChar('abcdefghijklmnopqrstuvwxyz'); result += rndChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ'); result += rndChar('0123456789'); result += rndChar(specialCharacters); for (let i = result.length; i < length; i++) { const charactersPlusSpecial = characters + specialCharacters; result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length)); } return shfChar(result); }, randomChar : rndChar, shuffleString: shfChar } })", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2023-08-11T19:57:02.536Z", + "_postman_exported_using": "Postman/10.17.0" +} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-mssql.postman_environment.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-mssql.postman_environment.json new file mode 100644 index 000000000..45a2ca205 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-mssql.postman_environment.json @@ -0,0 +1,57 @@ +{ + "id": "dbcd108a-4fc9-4266-9277-489c5c245e14", + "name": "Admin API Docker", + "values": [ + { + "key": "API_URL", + "value": "https://localhost/adminapi", + "type": "default", + "enabled": true + }, + { + "key": "offset", + "value": "0", + "type": "default", + "enabled": true + }, + { + "key": "limit", + "value": "100", + "type": "default", + "enabled": true + }, + { + "key": "connectionString", + "value": "Data Source=db-admin;Initial Catalog=EdFi_Admin;User Id=edfi;Password=P@55w0rd;Encrypt=false;TrustServerCertificate=true", + "type": "default", + "enabled": true + }, + { + "key": "securityconnectionString", + "value": "Data Source=db-admin;Initial Catalog=EdFi_Security;User Id=edfi;Password=P@55w0rd;Encrypt=false;TrustServerCertificate=true", + "type": "default", + "enabled": true + }, + { + "key": "isMultitenant", + "value": "false", + "type": "default", + "enabled": true + }, + { + "key": "sortByProperty", + "value": "function sortByProperty(objArray, prop, direction){ if (arguments.length<2) throw new Error(\"ARRAY, AND OBJECT PROPERTY MINIMUM ARGUMENTS, OPTIONAL DIRECTION\"); if (!Array.isArray(objArray)) throw new Error(\"FIRST ARGUMENT NOT AN ARRAY\"); const clone = objArray.slice(0); const direct = arguments.length>2 ? arguments[2] : 1; const propPath = (prop.constructor===Array) ? prop : prop.split(\".\"); clone.sort(function(a,b){ for (let p in propPath){ if (a[propPath[p]] && b[propPath[p]]){ a = a[propPath[p]]; b = b[propPath[p]]; } } a = a.match(/^\\d+$/) ? +a : a; b = b.match(/^\\d+$/) ? +b : b; return ( (a < b) ? -1*direct : ((a > b) ? 1*direct : 0) ); }); return clone; }", + "type": "default", + "enabled": true + }, + { + "key": "jsHelper", + "value": "(function () { const rndChar = function (str) { return str.charAt(Math.floor(Math.random() * str.length)); }; const shfChar = function (str) { const array = str.split(''); for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array.join(''); }; return { generateClientSecret: function(){ const minLength = 32; const maxLength = 128; let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~'; const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength; result += rndChar('abcdefghijklmnopqrstuvwxyz'); result += rndChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ'); result += rndChar('0123456789'); result += rndChar(specialCharacters); for (let i = result.length; i < length; i++) { const charactersPlusSpecial = characters + specialCharacters; result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length)); } return shfChar(result); }, randomChar : rndChar, shuffleString: shfChar } })", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2023-08-11T19:57:02.536Z", + "_postman_exported_using": "Postman/10.17.0" +} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-pgsql.postman_environment.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-pgsql.postman_environment.json new file mode 100644 index 000000000..5aa1e0328 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-pgsql.postman_environment.json @@ -0,0 +1,57 @@ +{ + "id": "dbcd108a-4fc9-4266-9277-489c5c245e14", + "name": "Admin API Docker", + "values": [ + { + "key": "API_URL", + "value": "https://localhost/adminapi", + "type": "default", + "enabled": true + }, + { + "key": "offset", + "value": "0", + "type": "default", + "enabled": true + }, + { + "key": "limit", + "value": "100", + "type": "default", + "enabled": true + }, + { + "key": "connectionString", + "value": "host=test;port=90;username=test;password=test;database=EdFi_Admin;pooling=false", + "type": "default", + "enabled": true + }, + { + "key": "securityconnectionString", + "value": "host=test;port=90;username=test;password=test;database=EdFi_Security;pooling=false", + "type": "default", + "enabled": true + }, + { + "key": "isMultitenant", + "value": "false", + "type": "default", + "enabled": true + }, + { + "key": "sortByProperty", + "value": "function sortByProperty(objArray, prop, direction){ if (arguments.length<2) throw new Error(\"ARRAY, AND OBJECT PROPERTY MINIMUM ARGUMENTS, OPTIONAL DIRECTION\"); if (!Array.isArray(objArray)) throw new Error(\"FIRST ARGUMENT NOT AN ARRAY\"); const clone = objArray.slice(0); const direct = arguments.length>2 ? arguments[2] : 1; const propPath = (prop.constructor===Array) ? prop : prop.split(\".\"); clone.sort(function(a,b){ for (let p in propPath){ if (a[propPath[p]] && b[propPath[p]]){ a = a[propPath[p]]; b = b[propPath[p]]; } } a = a.match(/^\\d+$/) ? +a : a; b = b.match(/^\\d+$/) ? +b : b; return ( (a < b) ? -1*direct : ((a > b) ? 1*direct : 0) ); }); return clone; }", + "type": "default", + "enabled": true + }, + { + "key": "jsHelper", + "value": "(function () { const rndChar = function (str) { return str.charAt(Math.floor(Math.random() * str.length)); }; const shfChar = function (str) { const array = str.split(''); for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array.join(''); }; return { generateClientSecret: function(){ const minLength = 32; const maxLength = 128; let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~'; const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength; result += rndChar('abcdefghijklmnopqrstuvwxyz'); result += rndChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ'); result += rndChar('0123456789'); result += rndChar(specialCharacters); for (let i = result.length; i < length; i++) { const charactersPlusSpecial = characters + specialCharacters; result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length)); } return shfChar(result); }, randomChar : rndChar, shuffleString: shfChar } })", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2023-08-11T19:57:02.536Z", + "_postman_exported_using": "Postman/10.17.0" +} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Actions.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Actions.postman_collection.json new file mode 100644 index 000000000..be1cf0639 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Actions.postman_collection.json @@ -0,0 +1,552 @@ +{ + "info": { + "_postman_id": "0b4c98f7-741f-4169-9fef-c96b83f92f6d", + "name": "Admin API E2E 2.0 - Actions", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "Actions", + "item": [ + { + "name": "Authstrategies", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Actions: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"GET Actions: Response matches success format\", function () {\r", + " if (response && response.length > 0) {\r", + " pm.expect(response[0]).to.have.property(\"id\");\r", + " pm.expect(response[0]).to.have.property(\"name\");\r", + " pm.expect(response[0]).to.have.property(\"uri\");\r", + " }\r", + "});\r", + "\r", + "const GetActionsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"uri\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"uri\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Actions: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetActionsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/actions?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "actions" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Authstrategies - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/actions?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "actions" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Authstrategies - Without Offset and Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Actions: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"GET Actions: Response matches success format\", function () {\r", + " if (response && response.length > 0) {\r", + " pm.expect(response[0]).to.have.property(\"id\");\r", + " pm.expect(response[0]).to.have.property(\"name\");\r", + " pm.expect(response[0]).to.have.property(\"uri\");\r", + " }\r", + "});\r", + "\r", + "const GetActionsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"uri\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"uri\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Actions: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetActionsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/actions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "actions" + ] + } + }, + "response": [] + }, + { + "name": "Authstrategies - Without Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Actions: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"GET Actions: Response matches success format\", function () {\r", + " if (response && response.length > 0) {\r", + " pm.expect(response[0]).to.have.property(\"id\");\r", + " pm.expect(response[0]).to.have.property(\"name\");\r", + " pm.expect(response[0]).to.have.property(\"uri\");\r", + " }\r", + "});\r", + "\r", + "const GetActionsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"uri\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"uri\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Actions: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetActionsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/actions?limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "actions" + ], + "query": [ + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Authstrategies - Without Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Actions: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"GET Actions: Response matches success format\", function () {\r", + " if (response && response.length > 0) {\r", + " pm.expect(response[0]).to.have.property(\"id\");\r", + " pm.expect(response[0]).to.have.property(\"name\");\r", + " pm.expect(response[0]).to.have.property(\"uri\");\r", + " }\r", + "});\r", + "\r", + "const GetActionsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"uri\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"uri\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Actions: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetActionsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/actions?offset={{offset}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "actions" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + } + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - ApiClient.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - ApiClient.postman_collection.json new file mode 100644 index 000000000..1dcf3d145 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - ApiClient.postman_collection.json @@ -0,0 +1,1607 @@ +{ + "info": { + "_postman_id": "caa580ae-af50-4717-9fd8-aeff2fe7d278", + "name": "Admin API E2E 2.0 - ApiClient", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "ApiClient", + "item": [ + { + "name": "ApiClients - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Application Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Application User\",\r", + " \"contactEmailAddress\": \"application@example.com\"\r", + " }), \r", + " }\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const id = response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"ApplicationVendorId\", id);\r", + " }\r", + "\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + " }, \r", + " function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json[0].id) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].id);\r", + " }\r", + "\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/applications`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"applicationName\": \"Test Application\",\r", + " \"vendorId\": pm.collectionVariables.get(\"ApplicationVendorId\"),\r", + " \"claimSetName\": \"Ed-Fi Sandbox\",\r", + " \"profileIds\": [],\r", + " \"educationOrganizationIds\": [ 255901 ],\r", + " \"odsInstanceIds\": [ pm.collectionVariables.get(\"ODSInstanceId\") ],\r", + " \"enabled\": true\r", + " }),\r", + " }\r", + " },\r", + " function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const id = response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"CreatedApplicationId\", id);\r", + " }\r", + " });\r", + " }); \r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test ApiClient\",\r\n \"isApproved\": true,\r\n \"applicationId\": {{CreatedApplicationId}},\r\n \"odsInstanceIds\": [\r\n {{ODSInstanceId}}\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/apiclients", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "apiclients" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Application Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Application User\",\r", + " \"contactEmailAddress\": \"application@example.com\"\r", + " }), \r", + " }\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const id = response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"ApplicationVendorId\", id);\r", + " }\r", + "\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + " }, \r", + " function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json[0].id) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].id);\r", + " }\r", + "\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/applications`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"applicationName\": \"Test Application\",\r", + " \"vendorId\": pm.collectionVariables.get(\"ApplicationVendorId\"),\r", + " \"claimSetName\": \"Ed-Fi Sandbox\",\r", + " \"profileIds\": [],\r", + " \"educationOrganizationIds\": [ 255901 ],\r", + " \"odsInstanceIds\": [ pm.collectionVariables.get(\"ODSInstanceId\") ],\r", + " \"enabled\": true\r", + " }),\r", + " }\r", + " },\r", + " function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const id = response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"CreatedApplicationId\", id);\r", + " }\r", + " });\r", + " }); \r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ApiClients: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"POST ApiClients: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " pm.response.to.be.header(\"Location\", `/apiclients/${result.id}`);\r", + "});\r", + "\r", + "pm.test(\"POST ApiClients: Response result includes api client key and secret\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + " pm.expect(result).to.have.property(\"key\");\r", + " pm.expect(result).to.have.property(\"secret\");\r", + " pm.expect(result).to.have.property(\"applicationId\");\r", + "});\r", + "\r", + "if(result.id) {\r", + " pm.collectionVariables.set(\"CreatedApiClientId\", result.id);\r", + "}\r", + "\r", + "const PostApiClientSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"key\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"secret\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"applicationId\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"key\",\r", + " \"secret\",\r", + " \"applicationId\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST ApiClient: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostApiClientSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test ApiClient\",\r\n \"isApproved\": true,\r\n \"applicationId\": {{CreatedApplicationId}},\r\n \"odsInstanceIds\": [\r\n {{ODSInstanceId}}\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/apiclients", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients - Invalid No Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ApiClients Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ApiClients Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ApiClients Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ApiClients Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"\",\r\n \"isApproved\": true,\r\n \"applicationId\": {{CreatedApplicationId}},\r\n \"odsInstanceIds\": [\r\n {{ODSInstanceId}}\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/apiclients", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients - Invalid No Application Id", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ApiClients Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ApiClients Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ApiClients Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ApiClients Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"ApplicationId\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Other Test ApiClient\",\r\n \"isApproved\": true,\r\n \"applicationId\": 0,\r\n \"odsInstanceIds\": [\r\n {{ODSInstanceId}}\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/apiclients", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients - Invalid No Ods Instances", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ApiClients Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ApiClients Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ApiClients Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ApiClients Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"OdsInstanceIds\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"\",\r\n \"isApproved\": true,\r\n \"applicationId\": {{CreatedApplicationId}},\r\n \"odsInstanceIds\": [ ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/apiclients", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients By Application", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ApiClients: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "if (Array.isArray(results) && results.length >= 2) {\r", + " const id1 = results[0].id;\r", + " const id2 = results[1].id;\r", + "\r", + " const csv = `${id1},${id2}`;\r", + " pm.collectionVariables.set(\"FirstTwoApiClientIdsCSV\", csv);\r", + "}\r", + "\r", + "pm.test(\"GET ApiClients: Response result includes api clients\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfApiClient = results.map(\r", + " function(apiclient) { return apiclient.id; }\r", + " ).indexOf(parseInt(pm.collectionVariables.get(\"CreatedApiClientId\"), 10));\r", + "\r", + " const result = results[indexOfApiClient];\r", + " pm.expect(result.name).to.equal(\"Test ApiClient\");\r", + " pm.expect(result.isApproved).to.equal(true);\r", + " pm.expect(result.applicationId).to.equal(parseInt(pm.collectionVariables.get(\"CreatedApplicationId\"), 10));\r", + " pm.expect(result.odsInstanceIds.length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"GET ApiClients: Response results do not include secret\", function () {\r", + " results.forEach(function(result, i) {\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + " });\r", + "});\r", + "\r", + "const GetApiClientsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"key\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isApproved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"useSandbox\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"applicationId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"sandboxType\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"keyStatus\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"key\",\r", + " \"name\",\r", + " \"isApproved\",\r", + " \"useSandbox\",\r", + " \"applicationId\",\r", + " \"sandboxType\",\r", + " \"keyStatus\",\r", + " \"educationOrganizationIds\",\r", + " \"odsInstanceIds\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ApiClients: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApiClientsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/apiclients?applicationid={{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients" + ], + "query": [ + { + "key": "applicationid", + "value": "{{CreatedApplicationId}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ApiClients By Application - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/apiclients?applicationid={{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "apiclients" + ], + "query": [ + { + "key": "applicationid", + "value": "{{CreatedApplicationId}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ApiClients by Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ApiClientId: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET ApiClientId: Response result matches api client\", function () {\r", + " pm.expect(result.name).to.equal(\"Test ApiClient\");\r", + " pm.expect(result.isApproved).to.equal(true);\r", + " pm.expect(result.applicationId).to.equal(parseInt(pm.collectionVariables.get(\"CreatedApplicationId\"), 10));\r", + " pm.expect(result.odsInstanceIds.length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"GET ApiClientId: Response result does not include key or secret\", function () { \r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + "});\r", + "\r", + "const GetApiClientSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"key\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isApproved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"useSandbox\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"applicationId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"sandboxType\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"keyStatus\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"key\",\r", + " \"name\",\r", + " \"isApproved\",\r", + " \"useSandbox\",\r", + " \"applicationId\",\r", + " \"sandboxType\",\r", + " \"keyStatus\",\r", + " \"educationOrganizationIds\",\r", + " \"odsInstanceIds\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ApiClientId: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApiClientSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/apiclients/{{CreatedApiClientId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients", + "{{CreatedApiClientId}}" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/apiclients/${pm.collectionVariables.get(\"CreatedApiClientId\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "\r", + "function (err, response) {\r", + " if(err) \r", + " { \r", + " console.log(\"Error :\", err); \r", + " } \r", + " const updated = response.json();\r", + " pm.test(\"PUT ApiClient: Response result includes updated api client\", function () {\r", + " pm.expect(updated.name).to.equal(\"Updated Test ApiClient\");\r", + " pm.expect(updated.isApproved).to.equal(false);\r", + " pm.expect(updated.applicationId).to.equal(parseInt(pm.collectionVariables.get(\"CreatedApplicationId\"), 10));\r", + " pm.expect(updated.odsInstanceIds.length).to.equal(1);\r", + " });\r", + "\r", + " pm.test(\"PUT ApiClient: Response result does not include application key and secret\", function () {\r", + " pm.expect(updated).to.not.have.property(\"secret\");\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Updated Test ApiClient\",\r\n \"isApproved\": false,\r\n \"applicationId\": {{CreatedApplicationId}},\r\n \"odsInstanceIds\": [\r\n {{ODSInstanceId}}\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/apiclients/{{CreatedApiClientId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients", + "{{CreatedApiClientId}}" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Updated Test ApiClient\",\r\n \"isApproved\": false,\r\n \"applicationId\": {{CreatedApplicationId}},\r\n \"odsInstanceIds\": [\r\n {{ODSInstanceId}}\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/apiclients/{{CreatedApiClientId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "apiclients", + "{{CreatedApiClientId}}" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients - Invalid No Ods Instances", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ApiClients Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ApiClients Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ApiClients Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ApiClients Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"OdsInstanceIds\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Updated Test ApiClient\",\r\n \"isApproved\": false,\r\n \"applicationId\": {{CreatedApplicationId}},\r\n \"odsInstanceIds\": [ ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/apiclients/{{CreatedApiClientId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients", + "{{CreatedApiClientId}}" + ] + } + }, + "response": [] + }, + { + "name": "Reset Credential", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ResetCredentials: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"PUT ResetCredentials: Response result includes application key and secret\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + " pm.expect(result).to.have.property(\"name\");\r", + " pm.expect(result).to.have.property(\"key\");\r", + " pm.expect(result).to.have.property(\"secret\");\r", + " pm.expect(result).to.have.property(\"applicationId\");\r", + "});\r", + "\r", + "const PutResetCredentialsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"key\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"secret\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"applicationId\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"key\",\r", + " \"secret\",\r", + " \"applicationId\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"PUT Reset Credentials: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PutResetCredentialsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/apiclients/{{CreatedApiClientId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients", + "{{CreatedApiClientId}}", + "reset-credential" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/apiclients/{{CreatedApiClientId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "apiclients", + "{{CreatedApiClientId}}" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE ApiClients: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DELETE ApiClients: Response matches success format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE ApiClients: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"apiclient\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "\r", + "const DeleteApiClientsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"DELETE ApiClients: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(DeleteApiClientsSchema);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/apiclients/{{CreatedApiClientId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients", + "{{CreatedApiClientId}}" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients by Id - Invalid Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ApiClients NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT ApiClients NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT ApiClients NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"apiclient\");\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/apiclients/{{CreatedApiClientId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients", + "{{CreatedApiClientId}}" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients - Invalid Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ApiClients NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT ApiClients NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT ApiClients NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"apiclient\");\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Updated Test ApiClient\",\r\n \"isApproved\": false,\r\n \"applicationId\": {{CreatedApplicationId}},\r\n \"odsInstanceIds\": [\r\n {{ODSInstanceId}}\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/apiclients/{{CreatedApiClientId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients", + "{{CreatedApiClientId}}" + ] + } + }, + "response": [] + }, + { + "name": "Reset Credential - Invalid Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ApiClients NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT ApiClients NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT ApiClients NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"ApiClient\");\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/apiclients/{{CreatedApiClientId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients", + "{{CreatedApiClientId}}", + "reset-credential" + ] + } + }, + "response": [] + }, + { + "name": "ApiClients - Invalid Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ApiClients NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT ApiClients NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT ApiClients NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"apiclient\");\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/apiclients/{{CreatedApiClientId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "apiclients", + "{{CreatedApiClientId}}" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "function generateClientSecret() {\r", + " const minLength = 32;\r", + " const maxLength = 128;\r", + " let result = '';\r", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\r", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';\r", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;\r", + "\r", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');\r", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');\r", + " result += randomChar('0123456789');\r", + " result += randomChar(specialCharacters);\r", + "\r", + " for (let i = result.length; i < length; i++) {\r", + " const charactersPlusSpecial = characters + specialCharacters;\r", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));\r", + " }\r", + "\r", + " return shuffleString(result);\r", + "}\r", + "\r", + "function randomChar(str) {\r", + " return str.charAt(Math.floor(Math.random() * str.length));\r", + "}\r", + "\r", + "function shuffleString(str) {\r", + " const array = str.split('');\r", + " for (let i = array.length - 1; i > 0; i--) {\r", + " const j = Math.floor(Math.random() * (i + 1));\r", + " [array[i], array[j]] = [array[j], array[i]];\r", + " }\r", + " return array.join('');\r", + "}\r", + "\r", + "let guid = pm.variables.replaceIn('{{$guid}}');\r", + "let client_secret = generateClientSecret();\r", + "\r", + "let header = {\r", + " 'Content-Type': 'application/x-www-form-urlencoded'\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'urlencoded',\r", + " urlencoded: [\r", + " {key: 'ClientId', value: guid },\r", + " {key: 'ClientSecret', value: client_secret },\r", + " {key: 'DisplayName', value: guid }\r", + " ]\r", + " }\r", + "},\r", + " (err, res) => {\r", + " error = res.json().error\r", + " if(error) {\r", + " throw res.json().error_description\r", + " }\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'urlencoded',\r", + " urlencoded: [\r", + " {key: 'client_id', value: guid },\r", + " {key: 'client_secret', value: client_secret },\r", + " {key: 'grant_type', value: \"client_credentials\"},\r", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}\r", + " ]\r", + " }\r", + "},\r", + " (err, res) => {\r", + " error = res.json().error\r", + " if(error) {\r", + " throw res.json().error_description\r", + " }\r", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)\r", + "});\r", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "", + "type": "string" + }, + { + "key": "ODSInstanceId", + "value": "", + "type": "string" + }, + { + "key": "ApplicationVendorId", + "value": "", + "type": "string" + }, + { + "key": "CreatedApplicationId", + "value": "", + "type": "string" + }, + { + "key": "FirstTwoApiClientIdsCSV", + "value": "", + "type": "string" + }, + { + "key": "CreatedApiClientId", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Application.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Application.postman_collection.json new file mode 100644 index 000000000..91fd47514 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Application.postman_collection.json @@ -0,0 +1,2390 @@ +{ + "info": { + "_postman_id": "f0a46a99-9881-40c6-b932-0827f614adc9", + "name": "Admin API E2E 2.0 - Application", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "Application", + "item": [ + { + "name": "Applications - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Application Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Application User\",\r", + " \"contactEmailAddress\": \"application@example.com\"\r", + " }), \r", + " }\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const id = response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"ApplicationVendorId\", id);\r", + " }\r", + "});\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json[0].id) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].id);\r", + " }\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceIds\": [ {{ODSInstanceId}} ],\r\n \"enabled\": true\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications" + ] + } + }, + "response": [] + }, + { + "name": "Applications", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Application Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Application User\",\r", + " \"contactEmailAddress\": \"application@example.com\"\r", + " }), \r", + " }\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const id = response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"ApplicationVendorId\", id);\r", + " }\r", + "});\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json[0].id) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].id);\r", + " }\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "\r", + "pm.test(\"POST Applications: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " pm.response.to.be.header(\"Location\", `/applications/${result.id}`);\r", + "});\r", + "\r", + "pm.test(\"POST Applications: Response result includes application key and secret\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + " pm.expect(result).to.have.property(\"key\");\r", + " pm.expect(result).to.have.property(\"secret\");\r", + "});\r", + "\r", + "if(result.id) {\r", + " pm.collectionVariables.set(\"CreatedApplicationId\", result.id);\r", + "}\r", + "\r", + "const PostApplicationSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"key\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"secret\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"key\",\r", + " \"secret\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Applications: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostApplicationSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceIds\": [ {{ODSInstanceId}} ],\r\n \"enabled\": true\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Applications Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"ApplicationName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ClaimSetName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"EducationOrganizationIds\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [],\r\n \"odsInstanceIds\": []\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Vendor", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications Invalid Vendor: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Applications Invalid Vendor: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid Vendor: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid Vendor: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.VendorId.length).to.equal(1);\r", + " pm.expect(response.errors.VendorId[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": 9999,\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceIds\": [ {{ODSInstanceId}} ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Profile", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications Invalid Profile: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"POST Applications Invalid Profile: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid Profile: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid Profile: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ProfileIds.length).to.equal(1);\r", + " pm.expect(response.errors.ProfileIds[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [ 9999 ],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceIds\": [ {{ODSInstanceId}} ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid OdsInstance", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications Invalid ODSInstance: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"POST Applications Invalid ODSInstance: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid ODSInstance: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid ODSInstance: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.OdsInstanceIds.length).to.equal(1);\r", + " pm.expect(response.errors.OdsInstanceIds[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [ ],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceIds\": [ 99999 ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, + { + "name": "Applications", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "if (Array.isArray(results) && results.length >= 2) {\r", + " const id1 = results[0].id;\r", + " const id2 = results[1].id;\r", + "\r", + " const csv = `${id1},${id2}`;\r", + " pm.collectionVariables.set(\"FirstTwoApplicationIdsCSV\", csv);\r", + "}\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfApplication = results.map(\r", + " function(application) { return application.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "\r", + " const result = results[indexOfApplication];\r", + " pm.expect(result.applicationName).to.equal(\"Test Application\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceIds.length).to.equal(1);\r", + " pm.expect(result.enabled).to.equal(true);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response results do not include key or secret\", function () {\r", + " results.forEach(function(result, i) {\r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + " });\r", + "});\r", + "\r", + "const GetApplicationsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"enabled\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceIds\",\r", + " \"enabled\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Applications: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApplicationsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/applications?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Applications - Without Offset and Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfApplication = results.map(\r", + " function(application) { return application.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "\r", + " const result = results[indexOfApplication];\r", + " pm.expect(result.applicationName).to.equal(\"Test Application\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceIds.length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response results do not include key or secret\", function () {\r", + " results.forEach(function(result, i) {\r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + " });\r", + "});\r", + "\r", + "const GetApplicationsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceIds\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Applications: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApplicationsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Without Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfApplication = results.map(\r", + " function(application) { return application.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "\r", + " const result = results[indexOfApplication];\r", + " pm.expect(result.applicationName).to.equal(\"Test Application\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceIds.length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response results do not include key or secret\", function () {\r", + " results.forEach(function(result, i) {\r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + " });\r", + "});\r", + "\r", + "const GetApplicationsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceIds\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Applications: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApplicationsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications?limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Applications - Without Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfApplication = results.map(\r", + " function(application) { return application.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "\r", + " const result = results[indexOfApplication];\r", + " pm.expect(result.applicationName).to.equal(\"Test Application\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceIds.length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response results do not include key or secret\", function () {\r", + " results.forEach(function(result, i) {\r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + " });\r", + "});\r", + "\r", + "const GetApplicationsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceIds\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Applications: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApplicationsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Applications by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ApplicationID: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET ApplicationID: Response result matches application\", function () {\r", + " const applicationId = pm.collectionVariables.get(\"CreatedApplicationId\");\r", + " pm.expect(result.id).to.equal(applicationId);\r", + " pm.expect(result.applicationName).to.equal(\"Test Application\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceIds.length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"GET ApplicationID: Response result does not include key or secret\", function () { \r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + "});\r", + "\r", + "const GetApplicationId = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceIds\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ApplicationId: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApplicationId);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications by Vendor", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Application by Vendor: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Application by Vendor: Response result includes applications\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfApplication = results.map(\r", + " function(application) { return application.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "\r", + " const result = results[indexOfApplication];\r", + " pm.expect(result.applicationName).to.equal(\"Test Application\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceIds.length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"GET Application by Vendor: Response result is filtered by vendor\", function () {\r", + " const resultApplicationIds = results.map(\r", + " function(application) { return application.id; }\r", + " );\r", + "\r", + " pm.expect(resultApplicationIds).to.contain(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + " pm.expect(resultApplicationIds).to.not.contain(pm.collectionVariables.get(\"OtherApplicationId\"));\r", + "});\r", + "\r", + "pm.test(\"GET Application by Vendor: Response results do not include key or secret\", function () {\r", + " results.forEach(function(result, i) {\r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + " });\r", + "});\r", + "\r", + "const GetApplicationByVendor = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceIds\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Application by Vendor: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApplicationByVendor);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Other Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Other Application User\",\r", + " \"contactEmailAddress\": \"otherapplication@example.com\"\r", + " }), \r", + " }\r", + "},\r", + "function (vendorErr, vendorResponse) {\r", + " if(vendorErr) { console.log(\"Error in Pre-request:\", vendorErr); }\r", + " const vendorId = vendorResponse.headers.get(\"Location\").split(\"/\")[2];\r", + " if (vendorId)\r", + " {\r", + " pm.collectionVariables.set(\"OtherApplicationVendorId\", vendorId);\r", + " }\r", + "\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/applications`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"applicationName\": \"Other Vendor Application\",\r", + " \"vendorId\": pm.collectionVariables.get(\"OtherApplicationVendorId\"),\r", + " \"claimSetName\": \"Ed-Fi Sandbox\",\r", + " \"profileIds\": [],\r", + " \"educationOrganizationIds\": [ 255901 ],\r", + " \"odsInstanceIds\": [pm.collectionVariables.get(\"ODSInstanceId\")],\r", + " \"enabled\" : true\r", + " }),\r", + " }\r", + " }, \r", + " function (appErr, appResonse) {\r", + " if(appErr) { console.log(\"Error in Pre-request:\", appErr); }\r", + " const id = appResonse.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"OtherApplicationId\", id);\r", + " }\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{ApplicationVendorId}}/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{ApplicationVendorId}}", + "applications" + ] + } + }, + "response": [] + }, + { + "name": "Applications by IDs", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ApplicationID: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET ApplicationID: Ensures the number of returned applications matches the number of requested IDs\", function () {\r", + " pm.expect(result.length).to.equal(2);\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications?ids={{FirstTwoApplicationIdsCSV}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "ids", + "value": "{{FirstTwoApplicationIdsCSV}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Applications", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/applications/${pm.collectionVariables.get(\"CreatedApplicationId\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (err, response) {\r", + " if(err) \r", + " { \r", + " console.log(\"Error :\", err); \r", + " } \r", + " const updated = response.json();\r", + "pm.test(\"PUT Application: Response result includes updated application\", function () {\r", + " pm.expect(updated.applicationName).to.equal(\"Updated Application Name\");\r", + " pm.expect(updated.claimSetName).to.equal(\"Ed-Fi ODS Admin App\");\r", + " pm.expect(updated.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(updated.profileIds.length).to.equal(0);\r", + " pm.expect(updated.odsInstanceIds.length).to.equal(1);\r", + " pm.expect(updated.enabled).to.equal(false);\r", + " });\r", + "\r", + " pm.test(\"PUT Application: Response result does not include application key and secret\", function () {\r", + " pm.expect(updated).to.not.have.property(\"key\");\r", + " pm.expect(updated).to.not.have.property(\"secret\");\r", + " });\r", + "\r", + "});\r", + "\r", + "pm.test(\"PUT Application: Request updated Application/Vendor relationship\", function () {\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors/${pm.collectionVariables.get(\"ApplicationVendorId\")}/applications`,\r", + " method: 'GET',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Application Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Application User\",\r", + " \"contactEmailAddress\": \"application@example.com\"\r", + " }), \r", + " }\r", + " }, \r", + " function (err, response) {\r", + " if(err) { console.log(\"Error in test request:\", err); }\r", + " if(response.code != 200) { console.log('Error in test request. Response is:', response); }\r", + " const results = response.json();\r", + " pm.expect(results.length).to.equal(0);\r", + " });\r", + "});\r", + "\r", + "\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Updated Application Name\",\r\n \"vendorId\": {{OtherApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi ODS Admin App\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [1234],\r\n \"odsInstanceIds\": [ {{ODSInstanceId}} ],\r\n \"enabled\": false\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Updated Application Name\",\r\n \"vendorId\": {{OtherApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi ODS Admin App\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [1234],\r\n \"odsInstanceIds\": [ {{ODSInstanceId}} ],\r\n \"enabled\": false\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT Application Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"ApplicationName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ClaimSetName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"EducationOrganizationIds\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"\",\r\n \"vendorId\": {{OtherApplicationVendorId}},\r\n \"claimSetName\": \"\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [],\r\n \"odsInstanceIds\": [ ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Vendor", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application Invalid Vendor: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT Application Invalid Vendor: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid Vendor: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid Vendor: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.VendorId.length).to.equal(1);\r", + " pm.expect(response.errors.VendorId[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Updated Application Name\",\r\n \"vendorId\": 9999,\r\n \"claimSetName\": \"Ed-Fi ODS Admin App\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [1234],\r\n \"odsInstanceIds\": [ {{ODSInstanceId}} ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Profile", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application Invalid Profile: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"PUT Application Invalid Profile: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid Profile: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid Profile: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ProfileIds.length).to.equal(1);\r", + " pm.expect(response.errors.ProfileIds[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Updated Application Name\",\r\n \"vendorId\": {{OtherApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi ODS Admin App\",\r\n \"profileIds\": [9999],\r\n \"educationOrganizationIds\": [1234],\r\n \"odsInstanceIds\": [ {{ODSInstanceId}} ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid OdsInstance", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application Invalid OdsInstance: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"PUT Application Invalid OdsInstance: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid OdsInstance: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid OdsInstance: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.OdsInstanceIds.length).to.equal(1);\r", + " pm.expect(response.errors.OdsInstanceIds[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Updated Application Name\",\r\n \"vendorId\": {{OtherApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi ODS Admin App\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [1234],\r\n \"odsInstanceIds\": [ 9999 ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Reset Credential", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ResetCredentials: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"PUT ResetCredentials: Response result includes application key and secret\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + " pm.expect(result).to.have.property(\"key\");\r", + " pm.expect(result).to.have.property(\"secret\");\r", + "});\r", + "\r", + "const PutResetCredentialsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"key\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"secret\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"key\",\r", + " \"secret\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"PUT Reset Credentials: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PutResetCredentialsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}", + "reset-credential" + ] + } + }, + "response": [] + }, + { + "name": "Reset Credential - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}", + "reset-credential" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DELETE Applications: Response matches success format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE Applications: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"application\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "\r", + "const DeleteApplicationsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"DELETE Applications: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(DeleteApplicationsSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Application NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Application NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Application NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"application\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Reset Credential - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Reset Credential NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT Reset Credential NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT Reset Credential NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"application\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}", + "reset-credential" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT Application NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"application\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationId\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceIds\": [ {{ODSInstanceId}} ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL Application NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"DEL Application NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DEL Application NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"application\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"ApplicationVendorId\");\r", + "pm.collectionVariables.unset(\"CreatedApplicationId\");\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "ODSInstanceId", + "value": "" + }, + { + "key": "OtherApplicationVendorId", + "value": "" + }, + { + "key": "OtherApplicationId", + "value": "" + }, + { + "key": "ApplicationVendorId", + "value": "" + }, + { + "key": "CreatedApplicationId", + "value": "" + }, + { + "key": "FirstTwoApplicationIdsCSV", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - AuthStrategies.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - AuthStrategies.postman_collection.json new file mode 100644 index 000000000..74ee32d35 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - AuthStrategies.postman_collection.json @@ -0,0 +1,255 @@ +{ + "info": { + "_postman_id": "460891ae-747c-427e-9858-aba36d433eef", + "name": "Admin API E2E 2.0 - AuthStrategies", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "AuthorizationStrategies", + "item": [ + { + "name": "AuthorizationStrategies", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET AuthorizationStrategies: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"GET AuthorizationStrategies: Response matches success format\", function () {\r", + " if (response && response.length > 0) {\r", + " pm.expect(response[0]).to.have.property(\"id\");\r", + " pm.expect(response[0]).to.have.property(\"name\");\r", + " pm.expect(response[0]).to.have.property(\"displayName\");\r", + " }\r", + "});\r", + "\r", + "const GetAuthStrategiesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"displayName\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET AuthorizationStrategies: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetAuthStrategiesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/authorizationStrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "authorizationStrategies" + ] + } + }, + "response": [] + }, + { + "name": "AuthorizationStrategies - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/authorizationStrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "authorizationStrategies" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Authorization.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Authorization.postman_collection.json new file mode 100644 index 000000000..b9e78d74a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Authorization.postman_collection.json @@ -0,0 +1,572 @@ +{ + "info": { + "_postman_id": "624eb590-9b5c-4b93-845a-d3be34a774ea", + "name": "Admin API E2E 2.0 - Authorization", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "42411115", + "_collection_link": "https://dewfdaf.postman.co/workspace/ADMINAPI-1295~b8166cce-f66f-4e17-acd8-c8f40ac88d5e/collection/42411115-624eb590-9b5c-4b93-845a-d3be34a774ea?action=share&source=collection_link&creator=42411115" + }, + "item": [ + { + "name": "User Management Authorization Scopes", + "item": [ + { + "name": "Register", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Register: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json().result;\r", + "\r", + "pm.test(\"POST Register: Response matches success format\", function () {\r", + " pm.expect(response.status).to.equal(200);\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"POST Register: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"client\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"registered\");\r", + "});\r", + "\r", + "const PostRegisterSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"status\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\",\r", + " \"status\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Register: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostRegisterSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " 'Content-Type': 'application/x-www-form-urlencoded'\r", + "};\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + "}\r", + "pm.collectionVariables.set(\"RegisteredClientId\", pm.variables.replaceIn('{{$guid}}'));\r", + "pm.collectionVariables.set(\"RegisteredClientSecret\", pm.variables.replaceIn('TKN{{$guid}}'));\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ClientId", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "ClientSecret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "DisplayName", + "value": "Postman Test", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/register", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Token - Full - Invalid Scope", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token: Status code is 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "pm.test(\"POST Token Invalid scope: Response with content type error\", function () {\r", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/problem+json\");\r", + "});\r", + "\r", + "const response = pm.response != null ? pm.response.json() : null;\r", + "\r", + "pm.test(\"POST Token: Response includes token\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + " \r", + "});\r", + "\r", + "\r", + "pm.test(\"Validate error response\", function () {\r", + " let jsonData = pm.response.json();\r", + " \r", + " // Check status code\r", + " pm.expect(pm.response.code).to.equal(400);\r", + " \r", + " // Validate response structure\r", + " pm.expect(jsonData).to.have.property(\"error\", \"invalid_scope\");\r", + " pm.expect(jsonData).to.have.property(\"error_description\", \"The request is missing required scope claims or has invalid scope values\");\r", + " \r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " 'Content-Type': 'application/x-www-form-urlencoded'\r", + "};\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + "}" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "invalid_scope", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Full - Access", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response != null ? pm.response.json() : null;\r", + "pm.collectionVariables.set(\"FULL_ACCESS_TOKEN\", response?.access_token);\r", + "\r", + "pm.test(\"POST Token: Response includes token\", function () {\r", + " pm.expect(response).to.have.property(\"access_token\");\r", + " pm.expect(response).to.have.property(\"token_type\");\r", + " pm.expect(response).to.have.property(\"expires_in\");\r", + "\r", + " pm.expect(response[\"token_type\"]).to.equal(\"Bearer\");\r", + "});\r", + "\r", + "const PostTokenSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"access_token\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"token_type\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"expires_in\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"access_token\",\r", + " \"token_type\",\r", + " \"expires_in\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Token: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostTokenSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " 'Content-Type': 'application/x-www-form-urlencoded'\r", + "};\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + "}" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/full_access", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Full Access", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSets: Response result includes claimsets\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "});\r", + "\r", + "const GetClaimSetsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSet: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " 'Content-Type': 'application/x-www-form-urlencoded'\r", + "};\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + "}" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{FULL_ACCESS_TOKEN}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "noauth" + } + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if(pm.request.auth && pm.request.auth.type === \"noauth\") {", + " return;", + "}", + "", + "let currentToken = pm.collectionVariables.get(\"TOKEN\");", + "if(currentToken) {", + " return;", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + " },", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: pm.variables.get(\"RegisteredClientId\")},", + " {key: 'client_secret', value: pm.variables.get(\"RegisteredClientSecret\")},", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "ODSInstanceId", + "value": "" + }, + { + "key": "NotExistClaimSetId", + "value": "" + }, + { + "key": "OtherApplicationVendorId", + "value": "" + }, + { + "key": "OtherApplicationId", + "value": "" + }, + { + "key": "ClaimSetGUID", + "value": "" + }, + { + "key": "CreatedClaimSetId", + "value": "" + }, + { + "key": "OtherClaimSetGUID", + "value": "" + }, + { + "key": "OtherExistingClaimSetId", + "value": "" + }, + { + "key": "RegisteredClientId", + "value": "" + }, + { + "key": "RegisteredClientSecret", + "value": "" + }, + { + "key": "ApplicationVendorId", + "value": "" + }, + { + "key": "CreatedApplicationId", + "value": "" + }, + { + "key": "FULL_ACCESS_TOKEN", + "value": "" + }, + { + "key": "TENANT_ACCESS_TOKEN", + "value": "" + }, + { + "key": "WORKER_PROCESS_TOKEN", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - ClaimSets.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - ClaimSets.postman_collection.json new file mode 100644 index 000000000..e5c851f9a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - ClaimSets.postman_collection.json @@ -0,0 +1,4794 @@ +{ + "info": { + "_postman_id": "e1980c65-4193-4699-82f5-73fdd9ad8d80", + "name": "Admin API E2E 2.0 - ClaimSets", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "1979934" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "ClaimSets", + "item": [ + { + "name": "ClaimSets - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"ClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test ClaimSet {{ClaimSetGUID}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/claimSets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimSets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"ClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " const id = pm.response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"CreatedClaimSetId\", id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test ClaimSet {{ClaimSetGUID}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Import - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"ImportClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaims\": [\r\n { \r\n \"name\": \"educationStandards\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n { \r\n \"name\": \"academicSubjectDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n } \r\n \r\n ],\r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n {\r\n \"name\": \"gradeLevelDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ],\r\n \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n { \r\n \"name\": \"publicationStatusDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n }\r\n ],\r\n \"name\": \"Test ClaimSet import {{ImportClaimSetGUID}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/claimSets/import", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimSets", + "import" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Import", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"ImportClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets - Import: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\"); \r", + " const locationHeader = pm.response.headers.get(\"Location\");\r", + " const id = locationHeader ? locationHeader.split(\"/\")[2] : null; // Added null check for locationHeader\r", + " if(id) {\r", + " pm.collectionVariables.set(\"ImportedClaimSetId\", id);\r", + " }\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Verify all resourceClaims were imported send request\", function(done){ // Added done parameter\r", + " let requestBody = pm.request.body ? pm.request.body.raw : null;\r", + " let requestData = requestBody ? JSON.parse(requestBody) : null;\r", + "\r", + " pm.sendRequest({\r", + " url: pm.environment.get(\"API_URL\") + \"/v2/claimSets/\" + pm.collectionVariables.get(\"ImportedClaimSetId\"),\r", + " method: \"GET\",\r", + " header:{\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": \"Bearer \" + pm.collectionVariables.get(\"TOKEN\")\r", + " }\r", + " }, function(err,res){\r", + " pm.expect(res).to.have.status(200);\r", + "\r", + " let importedClaimSetData = res.json();\r", + "\r", + " pm.test(\"POST ClaimSets - Import: Verify all resourceClaims were imported\",function(){\r", + " // Check that the number of resourceClaims matches the original request\r", + " if (requestData && requestData.resourceClaims) {\r", + " pm.expect(importedClaimSetData.resourceClaims.length).to.equal(requestData.resourceClaims.length);\r", + " }\r", + " // Check that the grandchild resourceClaim \"candidatePreparation\" was created\r", + " const hasCandidatePreparation = importedClaimSetData.resourceClaims.some(rc => rc.name === \"candidatePreparation\");\r", + " pm.expect(hasCandidatePreparation).to.be.true;\r", + " });\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaims\": [\r\n { \r\n \"name\": \"educationStandards\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n { \r\n \"name\": \"academicSubjectDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n } \r\n \r\n ],\r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n {\r\n \"name\": \"gradeLevelDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ],\r\n \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n { \r\n \"name\": \"publicationStatusDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n { \r\n \"name\": \"candidatePreparation\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n }\r\n ],\r\n \"name\": \"Test ClaimSet import {{ImportClaimSetGUID}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/import", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "import" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Import - Invalid Existing ClaimSet Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OtherClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"name\": `Other Test ClaimSet ${pm.collectionVariables.get(\"OtherClaimSetGUID\")}`,\r", + " \"resourceClaims\": []\r", + " }), \r", + " }\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const id = claimSetResponse.headers.get(\"Location\").split(\"/\")[2];\r", + " if(!id) { console.log('Error in Pre-request: claimset ID missing from response'); }\r", + " pm.collectionVariables.set(\"OtherExistingClaimSetId\", id);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets - Import: Invalid Existing ClaimSets: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Invalid Existing ClaimSets: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Invalid Existing ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Invalid Existing ClaimSets: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Name.length).to.equal(1);\r", + " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaims\": [\r\n { \r\n \"name\": \"educationStandards\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n { \r\n \"name\": \"academicSubjectDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n } \r\n \r\n ],\r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n {\r\n \"name\": \"gradeLevelDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ],\r\n \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n { \r\n \"name\": \"publicationStatusDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n }\r\n ],\r\n \"name\": \"Other Test ClaimSet {{OtherClaimSetGUID}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/import", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "import" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Import - Wrong resource name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"WrongNameClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets - Import: Wrong resouce name Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Wrong resouce name Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Wrong resouce name: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Wrong resouce name: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(1);\r", + " [\"not in the system\", \"educationStandards-123\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " }); \r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaims\": [\r\n { \r\n \"name\": \"educationStandards-123\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n { \r\n \"name\": \"academicSubjectDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n } \r\n \r\n ],\r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n {\r\n \"name\": \"gradeLevelDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ],\r\n \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n { \r\n \"name\": \"publicationStatusDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n }\r\n ],\r\n \"name\": \"Wrong Resouce name {{WrongNameClaimSetGUID}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/import", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "import" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Import - Invalid Wrong Parent Child Relationship", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"InvalidParentClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets - Import: Wrong relation: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Wrong relation: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Wrong relation: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Wrong relation: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(1);\r", + " [\"Child resource: 'academicSubjectDescriptor'\", \"wrong parent resource\", \"parent resource is: 'systemDescriptors'\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " }); \r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaims\": [\r\n { \r\n \"name\": \"educationStandards\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": [\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n } \r\n \r\n ],\r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"gradeLevelDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ],\r\n \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n { \r\n \"name\": \"publicationStatusDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n }\r\n ],\r\n \"name\": \"Wrong Relationship {{InvalidParentClaimSetGUID}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/import", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "import" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Import - Invalid Resource duplication", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"DuplicateClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets - Import: Resource duplication: name Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Resource duplication: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Resource duplication: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets - Import: Resource duplication: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(1);\r", + " [\"Only unique resource claims\", \"duplicate resource: 'gradeLevelDescriptor'\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " }); \r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaims\": [\r\n { \r\n \"name\": \"educationStandards\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n {\r\n \"name\": \"gradeLevelDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ],\r\n \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n {\r\n \"name\": \"gradeLevelDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ],\r\n \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n },\r\n {\r\n \"name\": \"publicationStatusDescriptor\",\r\n \"actions\": [\r\n {\r\n \"name\": \"Create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Update\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"Delete\",\r\n \"enabled\": true\r\n }\r\n ], \r\n \"authorizationStrategyOverridesForCRUD\": [],\r\n \"children\": []\r\n }\r\n ],\r\n \"name\": \"Wrong Relationship {{DuplicateClaimSetGUID}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/import", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "import" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid JSON", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Json: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Json: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Json: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Json: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \r\n\t\"noname\": \"Not-Valid\",\r\n \"window\": {\r\n \"title\": \"Sample Konfabulator Widget\",\r\n \"name\": \"main_window\",\r\n \"width\": 500,\r\n \"height\": 500\r\n },\r\n \"image\": { \r\n \"src\": \"Images/Sun.png\",\r\n \"name\": \"sun1\",\r\n \"hOffset\": 250,\r\n \"vOffset\": 250,\r\n \"alignment\": \"center\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Existing ClaimSet Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OtherClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"name\": `Other Test ClaimSet ${pm.collectionVariables.get(\"OtherClaimSetGUID\")}`,\r", + " \"resourceClaims\": []\r", + " }), \r", + " }\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const id = claimSetResponse.headers.get(\"Location\").split(\"/\")[2];\r", + " if(!id) { console.log('Error in Pre-request: claimset ID missing from response'); }\r", + " pm.collectionVariables.set(\"OtherExistingClaimSetId\", id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Name.length).to.equal(1);\r", + " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Other Test ClaimSet {{OtherClaimSetGUID}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSets: Response result includes claimsets\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "});\r", + "\r", + "const GetClaimSetsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSet: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/claimSets", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimSets" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Without Offset and Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSets: Response result includes claimsets\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "});\r", + "\r", + "const GetClaimSetsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSet: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Without Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSets: Response result includes claimsets\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "});\r", + "\r", + "const GetClaimSetsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSet: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets?limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Without Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSets: Response result includes claimsets\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "});\r", + "\r", + "const GetClaimSetsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSet: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSetsId: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSetsId: Response result matches claimset\", function () {\r", + " const claimSetId = pm.collectionVariables.get(\"CreatedClaimSetId\");\r", + " pm.expect(result.id).to.equal(parseInt(claimSetId));\r", + " pm.expect(result.name).to.equal(`Test ClaimSet ${pm.collectionVariables.get(\"ClaimSetGUID\")}`);\r", + " pm.expect(result._isSystemReserved).to.equal(false);\r", + " pm.expect(result._applications).to.be.empty;\r", + "});\r", + "\r", + "const GetClaimSetsIdSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"resourceClaims\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"actions\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"enabled\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " } \r", + " } ]\r", + " },\r", + " \"_defaultAuthorizationStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"actionId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"actionName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"authorizationStrategies\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " } \r", + "\r", + " }\r", + " }\r", + " ]\r", + " } \r", + " }\r", + " }\r", + " ]\r", + " },\r", + " \"authorizationStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"actionId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"actionName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"authorizationStrategies\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " }\r", + " }\r", + " ]\r", + " } \r", + " }\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"actions\",\r", + " \"_defaultAuthorizationStrategiesForCRUD\",\r", + " \"authorizationStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"resourceClaims\",\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSetId: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsIdSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"ClaimSets Export: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"ClaimSets Export: Response result matches claimset\", function () {\r", + " const claimSetId = pm.collectionVariables.get(\"CreatedClaimSetId\");\r", + " pm.expect(result.id).to.equal(parseInt(claimSetId));\r", + " pm.expect(result.name).to.equal(`Test ClaimSet ${pm.collectionVariables.get(\"ClaimSetGUID\")}`);\r", + " pm.expect(result._isSystemReserved).to.equal(false);\r", + " pm.expect(result._applications).to.be.empty;\r", + "});\r", + "\r", + "const GetClaimSetsIdSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"resourceClaims\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"actions\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"enabled\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " } \r", + " } ]\r", + " },\r", + " \"_defaultAuthorizationStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"actionId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"actionName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"authorizationStrategies\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " } \r", + "\r", + " }\r", + " }\r", + " ]\r", + " } \r", + " }\r", + " }\r", + " ]\r", + " },\r", + " \"authorizationStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"actionId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"actionName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"authorizationStrategies\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " }\r", + " }\r", + " ]\r", + " } \r", + " }\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"actions\",\r", + " \"_defaultAuthorizationStrategiesForCRUD\",\r", + " \"authorizationStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"resourceClaims\",\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"ClaimSets Export: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsIdSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/export", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "export" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Export - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/claimSets/{{CreatedClaimSetId}}/export", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimSets", + "{{CreatedClaimSetId}}", + "export" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Add Action", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResourceClaimAction: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4,\r\n \"resourceClaimActions\": [\r\n {\r\n \"name\": \"read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"create\",\r\n \"enabled\": true\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/resourceClaimActions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets by ID after adding Action", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSetsId after adding Action: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSetsId after adding Action: Response result matches claimset\", function () {\r", + " const claimSetId = pm.collectionVariables.get(\"CreatedClaimSetId\");\r", + " \r", + " pm.expect(result.id).to.equal(parseInt(claimSetId));\r", + " pm.expect(result.name).to.equal(`Test ClaimSet ${pm.collectionVariables.get(\"ClaimSetGUID\")}`);\r", + " pm.expect(result._isSystemReserved).to.equal(false);\r", + " pm.expect(result._applications).to.be.empty;\r", + " pm.expect(result.resourceClaims).to.not.be.empty;\r", + " const educationOrganizationsResourceClaim = result.resourceClaims.find(r => r.name === \"educationOrganizations\")\r", + " pm.expect(educationOrganizationsResourceClaim).to.be.an(\"object\", \"The educationOrganizations resource claim was not found.\")\r", + " const actionCreate = educationOrganizationsResourceClaim._defaultAuthorizationStrategiesForCRUD.find(r => r.actionName === \"Create\")\r", + " pm.expect(actionCreate).to.be.an(\"object\", \"The Create resource claim action was not found.\")\r", + "});\r", + "\r", + "const GetClaimSetsIdSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"resourceClaims\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"actions\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"enabled\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " } \r", + " } ]\r", + " },\r", + " \"_defaultAuthorizationStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"actionId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"actionName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"authorizationStrategies\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " } \r", + "\r", + " }\r", + " }\r", + " ]\r", + " } \r", + " }\r", + " }\r", + " ]\r", + " },\r", + " \"authorizationStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"actionId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"actionName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"authorizationStrategies\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " }\r", + " }\r", + " ]\r", + " } \r", + " }\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"actions\",\r", + " \"_defaultAuthorizationStrategiesForCRUD\",\r", + " \"authorizationStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"resourceClaims\",\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSetId after adding Action: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsIdSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Add Action Validation Errors", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResourceClaimAction Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ResourceClaimAction Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ResourceClaimAction Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ResourceClaimAction Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ResourceClaimActions.length).to.equal(2);\r", + " pm.expect(response.errors.ResourceClaimActions[0]).to.contain(\"which is not in the system\");\r", + " pm.expect(response.errors.ResourceClaimActions[1]).to.contain(\"have at least one action\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4000,\r\n \"resourceClaimActions\": [\r\n {\r\n \"name\": \"read\",\r\n \"enabled\": false\r\n },\r\n {\r\n \"name\": \"create\",\r\n \"enabled\": false\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/resourceClaimActions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Add duplicate action Validation Errors", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResourceClaimAction Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ResourceClaimAction Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ResourceClaimAction Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ResourceClaimAction Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ResourceClaimActions.length).to.equal(2);\r", + " pm.expect(response.errors.ResourceClaimActions[0]).to.contain(\"which is not in the system\");\r", + " pm.expect(response.errors.ResourceClaimActions[1]).to.contain(\"action is duplicated\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4000,\r\n \"resourceClaimActions\": [\r\n {\r\n \"name\": \"read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"read\",\r\n \"enabled\": true\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/resourceClaimActions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Add Action System Reserved", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSets = claimSetResponse.json();\r", + " if(!claimSets) { console.log('Error in Pre-request: ClaimSets missing from response.'); }\r", + " const systemReservedClaimSetIds = claimSets.map(\r", + " function(claimSet) { \r", + " if(claimSet._isSystemReserved)\r", + " {\r", + " return claimSet.id;\r", + " } \r", + " }\r", + " );\r", + " if(!systemReservedClaimSetIds) { console.log('Error in Pre-request: System Reserved claimset IDs not found. Response is:', claimSets); }\r", + " pm.collectionVariables.set(\"SystemReservedClaimSetId\", systemReservedClaimSetIds[0]);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResourceClaimAction System Reserved: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ResourceClaimAction System Reserved: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ResourceClaimAction System Reserved: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ResourceClaimAction System Reserved: Response errors include system reserved message\", function () {\r", + " pm.expect(response.errors.ClaimSetId.length).to.equal(1)\r", + " pm.expect(response.errors.ClaimSetId[0]).to.contain(\"system reserved\")\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4,\r\n \"resourceClaimActions\": [\r\n {\r\n \"name\": \"read\",\r\n \"enabled\": false\r\n },\r\n {\r\n \"name\": \"create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"update\",\r\n \"enabled\": false\r\n },\r\n {\r\n \"name\": \"delete\",\r\n \"enabled\": false\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{SystemReservedClaimSetId}}/resourceClaimActions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{SystemReservedClaimSetId}}", + "resourceClaimActions" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Modify Action", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ResourceClaimAction: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4,\r\n \"resourceClaimActions\": [\r\n {\r\n \"name\": \"read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"update\",\r\n \"enabled\": false\r\n },\r\n {\r\n \"name\": \"delete\",\r\n \"enabled\": false\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/resourceClaimActions/4", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions", + "4" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Modify Action System Reserved", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSets = claimSetResponse.json();\r", + " if(!claimSets) { console.log('Error in Pre-request: ClaimSets missing from response.'); }\r", + " const systemReservedClaimSetIds = claimSets.map(\r", + " function(claimSet) { \r", + " if(claimSet._isSystemReserved)\r", + " {\r", + " return claimSet.id;\r", + " } \r", + " }\r", + " );\r", + " if(!systemReservedClaimSetIds) { console.log('Error in Pre-request: System Reserved claimset IDs not found. Response is:', claimSets); }\r", + " pm.collectionVariables.set(\"SystemReservedClaimSetId\", systemReservedClaimSetIds[0]);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ResourceClaimAction System Reserved: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ResourceClaimAction System Reserved: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ResourceClaimAction System Reserved: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ResourceClaimAction System Reserved: Response errors include system reserved message\", function () {\r", + " pm.expect(response.errors.ClaimSetId.length).to.equal(1)\r", + " pm.expect(response.errors.ClaimSetId[0]).to.contain(\"system reserved\")\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4,\r\n \"resourceClaimActions\": [\r\n {\r\n \"name\": \"read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"update\",\r\n \"enabled\": false\r\n },\r\n {\r\n \"name\": \"delete\",\r\n \"enabled\": false\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{SystemReservedClaimSetId}}/resourceClaimActions/4", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{SystemReservedClaimSetId}}", + "resourceClaimActions", + "4" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Modify Action ClaimSet not found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ResourceClaimAction Not Found: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ResourceClaimAction Not Found: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT ResourceClaimAction Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"claimset\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4,\r\n \"resourceClaimActions\": [\r\n {\r\n \"name\": \"read\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"create\",\r\n \"enabled\": true\r\n },\r\n {\r\n \"name\": \"update\",\r\n \"enabled\": false\r\n },\r\n {\r\n \"name\": \"delete\",\r\n \"enabled\": false\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/90000/resourceClaimActions/4", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "90000", + "resourceClaimActions", + "4" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Override Auth Strategy", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OverrideAuthStrategy: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"actionName\": \"create\",\r\n \"authorizationStrategies\": [\r\n \"RelationshipsWithStudentsOnly\"\r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/resourceClaimActions/4/overrideAuthorizationStrategy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions", + "4", + "overrideAuthorizationStrategy" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Override Auth Strategy Default Strategy", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OverrideAuthStrategy: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"actionName\": \"create\",\r\n \"authorizationStrategies\": [\r\n \"RelationshipsWithStudentsOnly\", \"NoFurtherAuthorizationRequired\"\r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/resourceClaimActions/4/overrideAuthorizationStrategy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions", + "4", + "overrideAuthorizationStrategy" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets by ID after overriding Authorization Strategy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSetsId after overriding Authorization Strategy: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSetsId after overriding Authorization Strategy: Response result matches claimset\", function () {\r", + " const claimSetId = pm.collectionVariables.get(\"CreatedClaimSetId\");\r", + " \r", + " pm.expect(result.id).to.equal(parseInt(claimSetId));\r", + " pm.expect(result.name).to.equal(`Test ClaimSet ${pm.collectionVariables.get(\"ClaimSetGUID\")}`);\r", + " pm.expect(result._isSystemReserved).to.equal(false);\r", + " pm.expect(result._applications).to.be.empty;\r", + " pm.expect(result.resourceClaims).to.not.be.empty;\r", + "\r", + " const educationOrganizationsResourceClaim = result.resourceClaims.find(r => r.name === \"educationOrganizations\")\r", + " pm.expect(educationOrganizationsResourceClaim).to.be.an(\"object\", \"The educationOrganizations resource claim was not found.\")\r", + " pm.expect(educationOrganizationsResourceClaim.authorizationStrategyOverridesForCRUD).to.not.be.empty;\r", + " const overrideAuthCreate = educationOrganizationsResourceClaim.authorizationStrategyOverridesForCRUD.find(r => r.actionName === \"Create\")\r", + " pm.expect(overrideAuthCreate).to.be.an(\"object\", \"The Create authorization override was not found.\")\r", + " //validates that default wasn't added\r", + " pm.expect(overrideAuthCreate.authorizationStrategies.length).to.equal(1);\r", + " const relationshipsWithStudentsOnlyStrategy = overrideAuthCreate.authorizationStrategies.find(r => r.authStrategyName === \"RelationshipsWithStudentsOnly\")\r", + " pm.expect(relationshipsWithStudentsOnlyStrategy).to.be.an(\"object\", \"The RelationshipsWithStudentsOnly strategy name was not found.\")\r", + "});\r", + "\r", + "const GetClaimSetsIdSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"resourceClaims\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"actions\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"enabled\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " } \r", + " } ]\r", + " },\r", + " \"_defaultAuthorizationStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"actionId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"actionName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"authorizationStrategies\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " } \r", + "\r", + " }\r", + " }\r", + " ]\r", + " } \r", + " }\r", + " }\r", + " ]\r", + " },\r", + " \"authorizationStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"actionId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"actionName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"authorizationStrategies\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " }\r", + " }\r", + " ]\r", + " } \r", + " }\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"actions\",\r", + " \"_defaultAuthorizationStrategiesForCRUD\",\r", + " \"authorizationStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"resourceClaims\",\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSetId after overriding Authorization Strategy: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsIdSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Override Auth Strategy - Action not enabled", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OverrideAuthStrategy - Action not enabled: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy - Action not enabled: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy - Action not enabled: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy - Action not enabled: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Action.length).to.equal(1);\r", + " pm.expect(response.errors.Action[0]).to.contain(\"action is not enabled for the resource claim\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"actionName\": \"delete\",\r\n \"authorizationStrategies\": [\r\n \"RelationshipsWithStudentsOnly\"\r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/resourceClaimActions/4/overrideAuthorizationStrategy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions", + "4", + "overrideAuthorizationStrategy" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Override Auth Strategy System Reserved", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSets = claimSetResponse.json();\r", + " if(!claimSets) { console.log('Error in Pre-request: ClaimSets missing from response.'); }\r", + " const systemReservedClaimSetIds = claimSets.map(\r", + " function(claimSet) { \r", + " if(claimSet._isSystemReserved)\r", + " {\r", + " return claimSet.id;\r", + " } \r", + " }\r", + " );\r", + " if(!systemReservedClaimSetIds) { console.log('Error in Pre-request: System Reserved claimset IDs not found. Response is:', claimSets); }\r", + " pm.collectionVariables.set(\"SystemReservedClaimSetId\", systemReservedClaimSetIds[0]);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OverrideAuthStrategy System Reserved: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy System Reserved: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy System Reserved: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy System Reserved: Response errors include system reserved message\", function () {\r", + " pm.expect(response.errors.ClaimSetId.length).to.equal(1)\r", + " pm.expect(response.errors.ClaimSetId[0]).to.contain(\"system reserved\")\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"actionName\": \"create\",\r\n \"authorizationStrategies\": [\r\n \"RelationshipsWithStudentsOnly\"\r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{SystemReservedClaimSetId}}/resourceClaimActions/4/overrideAuthorizationStrategy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{SystemReservedClaimSetId}}", + "resourceClaimActions", + "4", + "overrideAuthorizationStrategy" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Override Auth Strategy Validation Errors", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OverrideAuthStrategy Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ResourceClaim.length).to.equal(1);\r", + " pm.expect(response.errors.ResourceClaim[0]).to.contain(\"doesn't exist\");\r", + " pm.expect(response.errors.AuthorizationStrategies.length).to.equal(1);\r", + " pm.expect(response.errors.AuthorizationStrategies[0]).to.contain(\"doesn't exist\");\r", + " pm.expect(response.errors.ActionName.length).to.equal(1);\r", + " pm.expect(response.errors.ActionName[0]).to.contain(\"doesn't exist\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"actionName\": \"NotExist\",\r\n \"authorizationStrategies\": [\"RelationshipsWithStudentsOnlys\"]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/resourceClaimActions/4000/overrideAuthorizationStrategy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions", + "4000", + "overrideAuthorizationStrategy" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Reset Authorization Strategies", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResetAuthorizationStrategies: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/resourceClaimActions/4/resetAuthorizationStrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions", + "4", + "resetAuthorizationStrategies" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Reset Authorization Strategies System Reserved", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSets = claimSetResponse.json();\r", + " if(!claimSets) { console.log('Error in Pre-request: ClaimSets missing from response.'); }\r", + " const systemReservedClaimSetIds = claimSets.map(\r", + " function(claimSet) { \r", + " if(claimSet._isSystemReserved)\r", + " {\r", + " return claimSet.id;\r", + " } \r", + " }\r", + " );\r", + " if(!systemReservedClaimSetIds) { console.log('Error in Pre-request: System Reserved claimset IDs not found. Response is:', claimSets); }\r", + " pm.collectionVariables.set(\"SystemReservedClaimSetId\", systemReservedClaimSetIds[0]);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResetAuthorizationStrategies System Reserved: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ResetAuthorizationStrategies System Reserved: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ResetAuthorizationStrategies System Reserved: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ResetAuthorizationStrategies System Reserved: Response errors include system reserved message\", function () {\r", + " pm.expect(response.errors.claimSetId.length).to.equal(1)\r", + " pm.expect(response.errors.claimSetId[0]).to.contain(\"system reserved\")\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{SystemReservedClaimSetId}}/resourceClaimActions/4/resetAuthorizationStrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{SystemReservedClaimSetId}}", + "resourceClaimActions", + "4", + "resetAuthorizationStrategies" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Reset Authorization Strategies Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResetAuthorizationStrategies: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/resourceClaimActions/40000/resetAuthorizationStrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions", + "40000", + "resetAuthorizationStrategies" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Delete - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/claimSets/{{CreatedClaimSetId}}/resourceClaimActions/4", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions", + "4" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Delete", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE ResourceClaimOnClaimSet: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/resourceClaimActions/4", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions", + "4" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Delete System Reserved", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSets = claimSetResponse.json();\r", + " if(!claimSets) { console.log('Error in Pre-request: ClaimSets missing from response.'); }\r", + " const systemReservedClaimSetIds = claimSets.map(\r", + " function(claimSet) { \r", + " if(claimSet._isSystemReserved)\r", + " {\r", + " return claimSet.id;\r", + " } \r", + " }\r", + " );\r", + " if(!systemReservedClaimSetIds) { console.log('Error in Pre-request: System Reserved claimset IDs not found. Response is:', claimSets); }\r", + " pm.collectionVariables.set(\"SystemReservedClaimSetId\", systemReservedClaimSetIds[0]);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE ResourceClaimOnClaimSet System Reserved: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DELETE ResourceClaimOnClaimSet System Reserved: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"DELETE ResourceClaimOnClaimSet System Reserved: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"DELETE ResourceClaimOnClaimSet System Reserved: Response errors include system reserved message\", function () {\r", + " pm.expect(response.errors.claimSetId.length).to.equal(1)\r", + " pm.expect(response.errors.claimSetId[0]).to.contain(\"system reserved\")\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{SystemReservedClaimSetId}}/resourceClaimActions/4", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{SystemReservedClaimSetId}}", + "resourceClaimActions", + "4" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Delete Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE ResourceClaimOnClaimSet: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/resourceClaimActions/4000", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "resourceClaimActions", + "4000" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets/Copy- Invalid Existing ClaimSet Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.collectionVariables.set(\"OtherClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"name\": `Other Test ClaimSet ${pm.collectionVariables.get(\"OtherClaimSetGUID\")}`,\r", + " \"resourceClaims\": []\r", + " }), \r", + " }\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const id = claimSetResponse.headers.get(\"Location\").split(\"/\")[2];\r", + " if(!id) { console.log('Error in Pre-request: claimset ID missing from response'); }\r", + " pm.collectionVariables.set(\"OtherExistingClaimSetId\", id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Name.length).to.equal(1);\r", + " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Other Test ClaimSet {{OtherClaimSetGUID}}\",\r\n \"resourceClaims\": []\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets/Copy", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets/${pm.collectionVariables.get(\"CreatedClaimSetId\")}/resourceClaimActions`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"resourceClaimId\": 9,\r", + " \"resourceClaimActions\": [\r", + " {\r", + " \"name\": \"read\",\r", + " \"enabled\": true\r", + " },\r", + " {\r", + " \"name\": \"create\",\r", + " \"enabled\": true\r", + " } \r", + " ]\r", + " })\r", + " }\r", + "},\r", + "function (claimSetErr) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); } \r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " const id = pm.response.headers.get(\"Location\").split(\"/\")[2];\r", + " pm.response.to.be.header(\"Location\", `/claimSets/${id}`);\r", + " pm.collectionVariables.set(\"CopiedClaimSetId\", id);\r", + " });\r", + "\r", + " let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimsets/${pm.collectionVariables.get(\"CopiedClaimSetId\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "\r", + "function (err, response) {\r", + "if(err) \r", + "{ \r", + " console.log(\"Error :\", err); \r", + "} \r", + " const result = response.json();\r", + " pm.test(\"POST ClaimSets: Response result claimset has expected name and resource claims\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + " pm.expect(result).to.have.property(\"name\");\r", + " pm.expect(result.name).contains(\"Copied ClaimSet from\");\r", + " pm.expect(result.resourceClaims).to.not.be.empty;\r", + " const resourceclaimexists = result.resourceClaims.any(r => r.name === \"educationStandards\")\r", + " pm.expect(resourceclaimexists).to.equal(true);\r", + " });\r", + "});\r", + "\r", + "\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Copied ClaimSet from {{CreatedClaimSetId}}\",\r\n \"originalid\": {{CreatedClaimSetId}}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/copy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "copy" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets/Copy - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets/${pm.collectionVariables.get(\"CreatedClaimSetId\")}/resourceClaimActions`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"resourceClaimId\": 9,\r", + " \"resourceClaimActions\": [\r", + " {\r", + " \"name\": \"read\",\r", + " \"enabled\": true\r", + " },\r", + " {\r", + " \"name\": \"create\",\r", + " \"enabled\": true\r", + " } \r", + " ]\r", + " })\r", + " }\r", + "},\r", + "function (claimSetErr) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); } \r", + "});\r", + "\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Copied ClaimSet from {{CreatedClaimSetId}}\",\r\n \"originalid\": {{CreatedClaimSetId}}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/claimSets/copy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimSets", + "copy" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets/Copy- Invalid ClaimSet Id", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "function randomIntFromInterval(min, max) { // min and max included \r", + " return Math.floor(Math.random() * (max - min + 1) + min)\r", + "}\r", + "\r", + "const rndInt = randomIntFromInterval(450, 783)\r", + "pm.collectionVariables.set(\"NotExistClaimSetId\", pm.variables.replaceIn(rndInt));\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSet NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSet NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response.title).to.contains(\"Not found\");\r", + " pm.expect(response.title).to.contains(\"claimset\"); \r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Claim set does not exists\",\r\n \"originalid\": {{NotExistClaimSetId}}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/copy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "copy" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimsets/${pm.collectionVariables.get(\"CreatedClaimSetId\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "\r", + "function (err, response) {\r", + " if(err) \r", + " { \r", + " console.log(\"Error :\", err); \r", + " } \r", + " const result = response.json();\r", + "pm.test(\"PUT ClaimSets: Response result includes updated claimset\", function () {\r", + " pm.expect(result.name).to.equal(\"Updated Test ClaimSet\");\r", + " pm.expect(result._isSystemReserved).to.equal(false);\r", + " pm.expect(result._applications).to.be.empty;\r", + " pm.expect(result.resourceClaims).to.not.be.empty;\r", + "});\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Updated Test ClaimSet\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Updated Test ClaimSet\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/claimSets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimSets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid JSON", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid Json: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Json: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Json: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Json: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}}, \r\n\t\"noname\": \"Not-Valid\",\r\n \"window\": {\r\n \"title\": \"Sample Konfabulator Widget\",\r\n \"name\": \"main_window\",\r\n \"width\": 500,\r\n \"height\": 500\r\n },\r\n \"image\": { \r\n \"src\": \"Images/Sun.png\",\r\n \"name\": \"sun1\",\r\n \"hOffset\": 250,\r\n \"vOffset\": 250,\r\n \"alignment\": \"center\"\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Existing ClaimSet Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid Existing ClaimSets: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Existing ClaimSets: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Existing ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Existing ClaimSets: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Name.length).to.equal(1);\r", + " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Other Test ClaimSet {{OtherClaimSetGUID}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - System Reserved", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSets = claimSetResponse.json();\r", + " if(!claimSets) { console.log('Error in Pre-request: ClaimSets missing from response.'); }\r", + " const systemReservedClaimSetIds = claimSets.map(\r", + " function(claimSet) { \r", + " if(claimSet._isSystemReserved)\r", + " {\r", + " return claimSet.id;\r", + " } \r", + " }\r", + " );\r", + " if(!systemReservedClaimSetIds) { console.log('Error in Pre-request: System Reserved claimset IDs not found. Response is:', claimSets); }\r", + " pm.collectionVariables.set(\"SystemReservedClaimSetId\", systemReservedClaimSetIds[0]);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets System Reserved: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets System Reserved: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets System Reserved: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets System Reserved: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"id\"].length).to.equal(1);\r", + " [\"AB Connect\", \"system reserved\"].forEach((substring) => {\r", + " pm.expect(response.errors.id[0]).to.contain(substring);\r", + " });\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Update System Reserved ClaimSet\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{SystemReservedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{SystemReservedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/claimSets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimSets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DELETE ClaimSets: Response matches success format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"claimset\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "\r", + "const DeleteClaimSetsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"DELETE ClaimSet: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(DeleteClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - System Reserved", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL ClaimSets System Reserved: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DEL ClaimSets System Reserved: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSets System Reserved: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSets System Reserved: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"id\"].length).to.equal(1);\r", + " [\"AB Connect\", \"system reserved\"].forEach((substring) => {\r", + " pm.expect(response.errors.id[0]).to.contain(substring);\r", + " });\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"SystemReservedClaimSetId\");\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{SystemReservedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{SystemReservedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - With Applications", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL ClaimSets With Application: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DEL ClaimSets With Application: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSets With Application: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSets With Application: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"id\"].length).to.equal(1);\r", + " [\"Cannot delete\", \"associated application\"].forEach((substring) => {\r", + " pm.expect(response.errors.id[0]).to.contain(substring);\r", + " });\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"OtherApplicationId\");\r", + "pm.collectionVariables.unset(\"OtherApplicationVendorId\");\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " \"company\": \"Other Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Other Application User\",\r", + " \"contactEmailAddress\": \"otherapplication@example.com\"\r", + " }),\r", + " }\r", + "},\r", + " function (vendorErr, vendorResponse) {\r", + " if (vendorErr) { console.log(\"Error in Pre-request:\", vendorErr); }\r", + " console.log(vendorResponse.headers.get(\"Location\"));\r", + " const vendorId = vendorResponse.headers.get(\"Location\").split(\"/\")[2];\r", + " if (!vendorId) { console.log('Error in Pre-request: vendorID missing from response'); }\r", + " pm.collectionVariables.set(\"OtherApplicationVendorId\", vendorId);\r", + "\r", + "\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + " },\r", + " function (err, response) {\r", + " if (err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if (!json[0].id) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].id);\r", + " }\r", + "\r", + "\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/applications`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " \"applicationName\": \"Other Vendor Application\",\r", + " \"vendorId\": pm.collectionVariables.get(\"OtherApplicationVendorId\"),\r", + " \"claimSetName\": \"Ed-Fi Sandbox\",\r", + " \"profileIds\": [],\r", + " \"educationOrganizationIds\": [255901],\r", + " \"odsInstanceIds\": [pm.collectionVariables.get(\"ODSInstanceId\")]\r", + " }),\r", + " }\r", + " },\r", + " function (appErr, appResonse) {\r", + " if (appErr) { console.log(\"Error in Pre-request:\", appErr); }\r", + " console.log(appResonse.headers.get(\"Location\"));\r", + " const appId = appResonse.headers.get(\"Location\").split(\"/\")[2];\r", + " if (!appId) { console.log('Error in Pre-request: applicationId missing from response'); }\r", + " else {\r", + " pm.collectionVariables.set(\"OtherApplicationId\", appId);\r", + " }\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/applications/${pm.collectionVariables.get(\"OtherApplicationId\")}`,\r", + " method: 'PUT',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({\r", + " \"applicationName\": \"ClaimSet Test Vendor Application\",\r", + " \"vendorId\": pm.collectionVariables.get(\"OtherApplicationVendorId\"),\r", + " \"claimSetName\": `Other Test ClaimSet ${pm.collectionVariables.get(\"OtherClaimSetGUID\")}`,\r", + " \"profileIds\": [],\r", + " \"educationOrganizationIds\": [255901],\r", + " \"odsInstanceIds\": [pm.collectionVariables.get(\"ODSInstanceId\")]\r", + " }),\r", + " }\r", + " },\r", + " function (appErr) {\r", + " if (appErr) { console.log(\"Error in Pre-request:\", appErr); }\r", + " });\r", + " });\r", + " });\r", + " });" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{OtherExistingClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{OtherExistingClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSet NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET ClaimSet NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"claimset\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedClaimSetId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Export - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSet NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET ClaimSet NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"claimset\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedClaimSetId\"));\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}/export", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}", + "export" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSet NotFound: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSet NotFound: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Edited-ClaimSet\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL ClaimSet NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSet NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"claimset\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedClaimSetId\"));\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"CreatedClaimSetId\");\r", + "pm.collectionVariables.unset(\"OtherExistingClaimSetId\");\r", + "pm.collectionVariables.unset(\"ClaimSetGUID\");\r", + "pm.collectionVariables.unset(\"OtherClaimSetGUID\");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "NotExistClaimSetId", + "value": "" + }, + { + "key": "RegisteredClientId", + "value": "" + }, + { + "key": "RegisteredClientSecret", + "value": "" + }, + { + "key": "ImportClaimSetGUID", + "value": "" + }, + { + "key": "WrongNameClaimSetGUID", + "value": "" + }, + { + "key": "InvalidParentClaimSetGUID", + "value": "" + }, + { + "key": "DuplicateClaimSetGUID", + "value": "" + }, + { + "key": "CopiedClaimSetId", + "value": "" + }, + { + "key": "ODSInstanceId", + "value": "" + }, + { + "key": "ClaimSetGUID", + "value": "" + }, + { + "key": "CreatedClaimSetId", + "value": "" + }, + { + "key": "ImportedClaimSetId", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Multitenant Isolation.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Multitenant Isolation.postman_collection.json new file mode 100644 index 000000000..4a1c28ba5 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Multitenant Isolation.postman_collection.json @@ -0,0 +1,605 @@ +{ + "info": { + "_postman_id": "def909b9-665e-47e4-8b78-a857711521c6", + "name": "Admin API E2E 2.0 - Multitenant Isolation", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "OdsInstances", + "item": [ + { + "name": "OdsInstances - Tenant1 - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OdsInstanceGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Tenant", + "value": "Tenant1", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test-OdsInstance-{{OdsInstanceGUID}}\",\r\n \"instanceType\": \"postgresql\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/odsInstances/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Tenant1", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OdsInstanceGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " pm.test(\"POST OdsInstances: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + " });\r", + "\r", + " pm.test(\"POST OdsInstances: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " const id = pm.response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"CreatedOdsInstanceId\", id);\r", + " }\r", + " });\r", + "}\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Tenant", + "value": "Tenant1", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test-OdsInstance-{{OdsInstanceGUID}}\",\r\n \"instanceType\": \"postgresql\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances by ID - Tenant1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " pm.test(\"GET OdsInstancesID: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + " });", + "", + " const response = pm.response.json();", + "", + " pm.test(\"GET OdsInstancesID: Response matches success format\", function () {", + " pm.expect(response).to.have.property(\"id\");", + " pm.expect(response).to.have.property(\"name\");", + " pm.expect(response).to.have.property(\"instanceType\");", + " pm.expect(response).to.have.property(\"odsInstanceContexts\");", + " pm.expect(response).to.have.property(\"odsInstanceDerivatives\");", + " });", + "", + " const GetOdsInstancesIdSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"instanceType\": {", + " \"type\": \"string\"", + " },", + " \"odsInstanceContexts\": {", + " \"type\": \"array\",", + " \"items\": {}", + " },", + " \"odsInstanceDerivatives\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"instanceType\",", + " \"odsInstanceContexts\",", + " \"odsInstanceDerivatives\"", + " ]", + " }", + "", + " pm.test(\"GET OdsInstancesID: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesIdSchema);", + " });", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Tenant", + "value": "Tenant1", + "type": "text" + } + ], + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{CreatedOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{CreatedOdsInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances by ID - Tenant1 - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + "});", + "", + "pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Tenant", + "value": "Tenant1", + "type": "text" + } + ], + "url": { + "raw": "{{API_URL}}/v1/odsInstances/{{CreatedOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances", + "{{CreatedOdsInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances by ID - Tenant2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " pm.test(\"GET OdsInstances NotFound: Status code is Not Found\", function () {", + " pm.response.to.have.status(404);", + " });", + "", + " pm.test(\"GET OdsInstances NotFound: Response matches error format\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response).to.have.property(\"title\");", + " });", + "", + " pm.test(\"GET OdsInstances NotFound: Response title is helpful and accurate\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response.title).to.contain(\"Not found\");", + " pm.expect(response.title).to.contain(\"odsInstance\");", + " });", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " function generateClientSecret() {\r", + " const minLength = 32;\r", + " const maxLength = 128;\r", + " let result = '';\r", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\r", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';\r", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;\r", + "\r", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');\r", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');\r", + " result += randomChar('0123456789');\r", + " result += randomChar(specialCharacters);\r", + "\r", + " for (let i = result.length; i < length; i++) {\r", + " const charactersPlusSpecial = characters + specialCharacters;\r", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));\r", + " }\r", + "\r", + " return shuffleString(result);\r", + " }\r", + "\r", + " function randomChar(str) {\r", + " return str.charAt(Math.floor(Math.random() * str.length));\r", + " }\r", + "\r", + " function shuffleString(str) {\r", + " const array = str.split('');\r", + " for (let i = array.length - 1; i > 0; i--) {\r", + " const j = Math.floor(Math.random() * (i + 1));\r", + " [array[i], array[j]] = [array[j], array[i]];\r", + " }\r", + " return array.join('');\r", + " }\r", + "\r", + " let guid = pm.variables.replaceIn('{{$guid}}');\r", + " let client_secret = generateClientSecret();\r", + "\r", + " let header = {\r", + " 'Content-Type': 'application/x-www-form-urlencoded'\r", + " };\r", + "\r", + " if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant2\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant2\")}` });\r", + " }\r", + "\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'urlencoded',\r", + " urlencoded: [\r", + " {key: 'ClientId', value: guid },\r", + " {key: 'ClientSecret', value: client_secret },\r", + " {key: 'DisplayName', value: guid }\r", + " ]\r", + " }\r", + " },\r", + " (err, res) => {\r", + " error = res.json().error\r", + " if(error) {\r", + " throw res.json().error_description\r", + " }\r", + "\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'urlencoded',\r", + " urlencoded: [\r", + " {key: 'client_id', value: guid },\r", + " {key: 'client_secret', value: client_secret },\r", + " {key: 'grant_type', value: \"client_credentials\"},\r", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}\r", + " ]\r", + " }\r", + " },\r", + " (err, res) => {\r", + " error = res.json().error\r", + " if(error) {\r", + " throw res.json().error_description\r", + " }\r", + " pm.collectionVariables.set(\"TOKEN_TENANT2\", res.json().access_token)\r", + " });\r", + " });\r", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN_TENANT2}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Tenant", + "value": "Tenant2", + "type": "text" + } + ], + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{CreatedOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{CreatedOdsInstanceId}}" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN_TENANT1}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN_TENANT1\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "OdsInstanceGUID", + "value": "" + }, + { + "key": "TOKEN_TENANT1", + "value": "" + }, + { + "key": "TOKEN_TENANT2", + "value": "" + }, + { + "key": "CreatedOdsInstanceId", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - OdsInstanceContexts.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - OdsInstanceContexts.postman_collection.json new file mode 100644 index 000000000..d48368c67 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - OdsInstanceContexts.postman_collection.json @@ -0,0 +1,1700 @@ +{ + "info": { + "_postman_id": "46f6bde0-6565-4676-b8cc-655a4aec2d01", + "name": "Admin API E2E 2.0 - OdsInstancesContexts", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "OdsInstanceContexts", + "item": [ + { + "name": "OdsInstanceContexts - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json[0].id) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].id);\r", + " }\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"contextKey\": \"ckey\",\r\n \"contextValue\": \"cvalue\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/OdsInstanceContexts/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "OdsInstanceContexts", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json[0].id) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ODS Instance Contexts: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test(\"POST ODS Instance Contexts: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " const id = pm.response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"CreatedOdsInstanceContextId\", id);\r", + " }\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"contextKey\": \"ckey\",\r\n \"contextValue\": \"cvalue\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ODS Instance Contexts Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ODS Instance Contexts Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ODS Instance Contexts Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"contextKey\": \"\",\r\n \"contextValue\": \"cvalue\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Invalid ODS Instance", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ODS Instance Contexts Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ODS Instance Contexts Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ODS Instance Contexts Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ODS Instance Contexts Invalid ODSInstance: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.OdsInstanceId.length).to.equal(1);\r", + " pm.expect(response.errors.OdsInstanceId[0]).to.contain(\"provide valid ods instance id.\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": 0,\r\n \"contextKey\": \"ckey\",\r\n \"contextValue\": \"cvalue\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Not Found ODS Instance", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ODS Instance Contexts Invalid: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ODS Instance Contexts Invalid: Response matches title property\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "\r", + "pm.test(\"POST ODS Instance Contexts Invalid ODSInstance: Response errors include messages by property\", function () {\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"odsInstance\");\r", + " pm.expect(response.title).to.contain(\"It may have been recently deleted\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": 9000,\r\n \"contextKey\": \"ckey\",\r\n \"contextValue\": \"cvalue\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Invalid Combined Key", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ODS Instance Contexts Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ODS Instance Contexts Invalid: Response matches title property\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "\r", + "pm.test(\"POST ODS Instance Contexts Invalid ODSInstance: Response errors combined key validation\", function () {\r", + " pm.expect(response.title).to.contain(\"Validation failed\");\r", + " pm.expect(response.errors).to.be.an(\"object\").and.not.to.be.empty;\r", + " pm.expect(response.errors[\"\"]).to.be.an(\"array\").that.includes(\"The combined key ODS instance id and context key must be unique.\");\r", + "});\r", + "\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"contextKey\": \"ckey\",\r\n \"contextValue\": \"cvalue\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstanceContexts: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceContexts: Response result includes OdsInstancesContext\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetOdsInstanceContextsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"contextKey\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contextValue\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"contextKey\",\r", + " \"contextValue\",\r", + " \"odsInstanceId\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET OdsInstanceContexts: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetOdsInstanceContextsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/OdsInstanceContexts", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "OdsInstanceContexts" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Without Offset and Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstanceContexts: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceContexts: Response result includes OdsInstancesContext\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetOdsInstanceContextsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"contextKey\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contextValue\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"contextKey\",\r", + " \"contextValue\",\r", + " \"odsInstanceId\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET OdsInstanceContexts: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetOdsInstanceContextsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Without Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstanceContexts: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceContexts: Response result includes OdsInstancesContext\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetOdsInstanceContextsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"contextKey\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contextValue\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"contextKey\",\r", + " \"contextValue\",\r", + " \"odsInstanceId\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET OdsInstanceContexts: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetOdsInstanceContextsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/?limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "" + ], + "query": [ + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Without Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstanceContexts: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceContexts: Response result includes OdsInstancesContext\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetOdsInstanceContextsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"contextKey\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contextValue\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"contextKey\",\r", + " \"contextValue\",\r", + " \"odsInstanceId\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET OdsInstanceContexts: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetOdsInstanceContextsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/?offset={{offset}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstanceContextById: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceContextId: Response result matches OdsInstancesContext\", function () {\r", + " const result = pm.response.json();\r", + " pm.expect(result.contextKey).to.not.be.empty;\r", + " pm.expect(result.contextValue).to.not.be.empty;\r", + " pm.expect(result.odsInstanceId).to.not.equal(0);\r", + "});\r", + "\r", + "const GetOdsInstanceContextsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"contextKey\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contextValue\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"contextKey\",\r", + " \"contextValue\",\r", + " \"odsInstanceId\" \r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET OdsInstancesContext: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetOdsInstanceContextsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/{{CreatedOdsInstanceContextId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "{{CreatedOdsInstanceContextId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts by ID - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstanceContexts NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceContexts NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceContexts NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"odsInstanceContext\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistOdsInstancesContextId\"));\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/{{NotExistOdsInstancesContextId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "{{NotExistOdsInstancesContextId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceContexts: Status code is Created\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/OdsInstanceContexts/${pm.collectionVariables.get(\"CreatedOdsInstanceContextId\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (err, response) {\r", + " if(err) \r", + " { \r", + " console.log(\"Error :\", err); \r", + " } \r", + " const updatedJson = response.json();\r", + " pm.test(\"PUT OdsInstanceContexts: Response includes updated OdsInstanceContext\", function () {\r", + " pm.expect(updatedJson.contextKey).to.equal(\"ckey\");\r", + " pm.expect(updatedJson.contextValue).to.be.not.empty;\r", + " pm.expect(updatedJson.odsInstanceId).to.be.not.equal(0);\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"contextKey\": \"ckey\",\r\n \"contextValue\": \"cvalue\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/{{CreatedOdsInstanceContextId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "{{CreatedOdsInstanceContextId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"contextKey\": \"ckey\",\r\n \"contextValue\": \"cvalue\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/OdsInstanceContexts/{{CreatedOdsInstanceContextId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "OdsInstanceContexts", + "{{CreatedOdsInstanceContextId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceContexts NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstanceContexts NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstanceContexts NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"odsInstanceContext\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistOdsInstancesContextId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"contextKey\": \"ckey3\",\r\n \"contextValue\": \"cvalue3\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/{{NotExistOdsInstancesContextId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "{{NotExistOdsInstancesContextId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceContexts Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT OdsInstanceContexts Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstanceContexts Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": 0,\r\n \"contextKey\": \"\",\r\n \"contextValue\": \"cvalue\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/{{CreatedOdsInstanceContextId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "{{CreatedOdsInstanceContextId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Invalid ODS Instance Id", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceContexts Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT OdsInstanceContexts Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstanceContexts Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstanceContexts Invalid: Invalid OdsInstanceId\", function () {\r", + " pm.expect(response.errors.OdsInstanceId[0]).to.contain(\"valid ods instance id\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": 0,\r\n \"contextKey\": \"ckey\",\r\n \"contextValue\": \"cvalue\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/{{CreatedOdsInstanceContextId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "{{CreatedOdsInstanceContextId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Not Found ODS Instance Id", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceContexts Invalid: Status code is not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT OdsInstanceContexts Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "\r", + "pm.test(\"PUT ODS Instance Contexts Invalid ODSInstance: Response errors include messages by property\", function () {\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"odsInstance\");\r", + " pm.expect(response.title).to.contain(\"It may have been recently deleted\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": 9000,\r\n \"contextKey\": \"ckey\",\r\n \"contextValue\": \"cvalue\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/{{CreatedOdsInstanceContextId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "{{CreatedOdsInstanceContextId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Invalid Combined Key", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstanceContexts/`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"odsInstanceId\": `${pm.collectionVariables.get(\"ODSInstanceId\")}`,\r", + " \"contextKey\": \"ckey2\",\r", + " \"contextValue\": \"cvalue2\"\r", + " }), \r", + " }\r", + "},\r", + "function (error, response) {\r", + " if(error) { console.log(\"Error in Pre-request:\", error); }\r", + " const id = response.headers.get(\"Location\").split(\"/\")[2];\r", + " pm.collectionVariables.set(\"OtherOdsInstanceContextId\", id);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceContexts Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + " const response = pm.response.json();\r", + " \r", + "pm.test(\"PUT OdsInstanceContexts Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "\r", + "pm.test(\"PUT ODS Instance Contexts Invalid ODSInstance: Response errors combined key validation\", function () {\r", + " pm.expect(response.title).to.contain(\"Validation failed\");\r", + " pm.expect(response.errors).to.be.an(\"object\").and.not.to.be.empty;\r", + " pm.expect(response.errors[\"\"]).to.be.an(\"array\").that.includes(\"The combined key ODS instance id and context key must be unique.\");\r", + " });" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"contextKey\": \"ckey2\",\r\n \"contextValue\": \"cvalue\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/{{CreatedOdsInstanceContextId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "{{CreatedOdsInstanceContextId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/OdsInstanceContexts/{{CreatedOdsInstanceContextId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "OdsInstanceContexts", + "{{CreatedOdsInstanceContextId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE OdsInstanceContexts: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/{{CreatedOdsInstanceContextId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "{{CreatedOdsInstanceContextId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceContexts - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE OdsInstanceContexts NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"DELETE OdsInstanceContexts NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE OdsInstanceContexts NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"odsInstanceContext\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistOdsInstancesContextId\"));\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceContexts/{{NotExistOdsInstancesContextId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceContexts", + "{{NotExistOdsInstancesContextId}}" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function randomIntFromInterval(min, max) { // min and max included ", + " return Math.floor(Math.random() * (max - min + 1) + min)", + "}", + "", + "const rndInt = randomIntFromInterval(450, 783)", + "pm.collectionVariables.set(\"NotExistProfileId\", pm.variables.replaceIn(rndInt));" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "NotExistProfileId", + "value": "" + }, + { + "key": "CreatedProfileId", + "value": "" + }, + { + "key": "ODSInstanceId", + "value": "" + }, + { + "key": "CreatedOdsInstanceDerivativeId", + "value": "" + }, + { + "key": "NotExistOdsInstancesDerivativeId", + "value": "90" + }, + { + "key": "CreatedOdsInstanceContextId", + "value": "" + }, + { + "key": "NotExistOdsInstancesContextId", + "value": "786" + }, + { + "key": "OtherOdsInstanceContextId", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - OdsInstanceDerivatives.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - OdsInstanceDerivatives.postman_collection.json new file mode 100644 index 000000000..a5b3ffc8c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - OdsInstanceDerivatives.postman_collection.json @@ -0,0 +1,1935 @@ +{ + "info": { + "_postman_id": "3d6c8fa8-dabb-4412-afee-f65e6438bd26", + "name": "Admin API E2E 2.0 - OdsInstancesDerivatives", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "OdsInstanceDerivatives", + "item": [ + { + "name": "OdsInstanceDerivatives", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json[0].id) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ODS Instance Derivatives: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " const id = pm.response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"CreatedOdsInstanceDerivativeId\", id);\r", + " }\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"derivativeType\": \"ReadReplica\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json[0].id) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].id);\r", + " }\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"derivativeType\": \"ReadReplica\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/OdsInstanceDerivatives/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "OdsInstanceDerivatives", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ODS Instance Derivatives Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"derivativeType\": \"\",\r\n \"connectionString\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid Derivative Type", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ODS Instance Derivatives Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid: DerivativeType included\", function () {\r", + " pm.expect(response.errors).to.have.property('DerivativeType');\r", + "});\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid: DerivativeType value allowed\", function () {\r", + " pm.expect(response.errors.DerivativeType[0]).to.contain(\"The value for the Derivative type is not allowed. The only accepted values are: 'ReadReplica' or 'Snapshot'\");\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"derivativeType\": \"another-derivative-type\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid ConnectionString", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ODS Instance Derivatives Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid: ConnectionString included\", function () {\r", + " pm.expect(response.errors).to.have.property('ConnectionString');\r", + "});\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid: ConnectionString value allowed\", function () {\r", + " pm.expect(response.errors.ConnectionString[0]).to.contain(\"The connection string is not valid.\");\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"derivativeType\": \"Snapshot\",\r\n \"connectionString\": \"invalid-connection-string\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid ODS Instance", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ODS Instance Derivatives Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid ODSInstance: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.OdsInstanceId.length).to.equal(1);\r", + " pm.expect(response.errors.OdsInstanceId[0]).to.contain(\"provide valid ods instance id.\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": 0,\r\n \"derivativeType\": \"ReadReplica\",\r\n \"connectionString\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Not Found ODS Instance", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ODS Instance Derivatives Invalid: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid: Response matches title property\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid ODSInstance: Response errors include messages by property\", function () {\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"odsInstance\");\r", + " pm.expect(response.title).to.contain(\"It may have been recently deleted\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": 9000,\r\n \"derivativeType\": \"ReadReplica\",\r\n \"connectionString\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid Combined Key", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ODS Instance Derivatives Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid: Response matches title property\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "\r", + "pm.test(\"POST ODS Instance Derivatives Invalid ODSInstance: Response errors include messages by property\", function () {\r", + " pm.expect(response.title).to.contain(\"Validation failed\");\r", + " pm.expect(response.errors).to.be.an(\"object\").and.not.to.be.empty;\r", + " pm.expect(response.errors[\"\"]).to.be.an(\"array\").that.includes(\"The combined key ODS instance id and derivative type must be unique.\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"derivativeType\": \"ReadReplica\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstanceDerivatives: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceDerivatives: Response result includes OdsInstancesDerivative\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetOdsInstanceDerivativesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"derivativeType\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"connectionString\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"derivativeType\",\r", + " \"odsInstanceId\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET OdsInstanceDerivatives: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetOdsInstanceDerivativesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/OdsInstanceDerivatives", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "OdsInstanceDerivatives" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Without Offset and Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstanceDerivatives: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceDerivatives: Response result includes OdsInstancesDerivative\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetOdsInstanceDerivativesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"derivativeType\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"connectionString\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"derivativeType\",\r", + " \"odsInstanceId\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET OdsInstanceDerivatives: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetOdsInstanceDerivativesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Without Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstanceDerivatives: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceDerivatives: Response result includes OdsInstancesDerivative\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetOdsInstanceDerivativesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"derivativeType\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"connectionString\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"derivativeType\",\r", + " \"odsInstanceId\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET OdsInstanceDerivatives: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetOdsInstanceDerivativesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/?offset={{offset}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Without Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstanceDerivatives: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceDerivatives: Response result includes OdsInstancesDerivative\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetOdsInstanceDerivativesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"derivativeType\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"connectionString\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"derivativeType\",\r", + " \"odsInstanceId\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET OdsInstanceDerivatives: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetOdsInstanceDerivativesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/?limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "" + ], + "query": [ + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstanceDerivativesById: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceDerivativesId: Response result matches OdsInstancesDerivative\", function () {\r", + " const result = pm.response.json();\r", + " pm.expect(result.derivativeType).to.not.be.empty;\r", + " pm.expect(result.odsInstanceId).to.not.equal(0);\r", + "});\r", + "\r", + "const GetOdsInstanceDerivativesSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"derivativeType\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"connectionString\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"derivativeType\",\r", + " \"odsInstanceId\" \r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET OdsInstancesDerivative: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetOdsInstanceDerivativesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/{{CreatedOdsInstanceDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "{{CreatedOdsInstanceDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives by ID - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstanceDerivatives NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceDerivatives NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET OdsInstanceDerivatives NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"odsInstanceDerivative\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistOdsInstancesDerivativeId\"));\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/{{NotExistOdsInstancesDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "{{NotExistOdsInstancesDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceDerivatives: Status code is Created\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/OdsInstanceDerivatives/${pm.collectionVariables.get(\"CreatedOdsInstanceDerivativeId\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (err, response) {\r", + " if(err) \r", + " { \r", + " console.log(\"Error :\", err); \r", + " } \r", + " const updatedJson = response.json();\r", + " pm.test(\"PUT OdsInstanceDerivatives: Response includes updated OdsInstanceDerivative\", function () {\r", + " pm.expect(updatedJson.derivativeType).to.equal(\"ReadReplica\");\r", + " pm.expect(updatedJson.odsInstanceId).to.be.not.equal(0);\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"derivativeType\": \"ReadReplica\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/{{CreatedOdsInstanceDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "{{CreatedOdsInstanceDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"derivativeType\": \"ReadReplica\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/OdsInstanceDerivatives/{{CreatedOdsInstanceDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "OdsInstanceDerivatives", + "{{CreatedOdsInstanceDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceDerivatives NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstanceDerivatives NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstanceDerivatives NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"odsInstanceDerivative\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistOdsInstancesDerivativeId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"derivativeType\": \"Snapshot\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/{{NotExistOdsInstancesDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "{{NotExistOdsInstancesDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": 0,\r\n \"derivativeType\": \"\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/{{CreatedOdsInstanceDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "{{CreatedOdsInstanceDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid Derivative Type", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ODS Instance Derivatives Invalid: DerivativeType included\", function () {\r", + " pm.expect(response.errors).to.have.property('DerivativeType');\r", + "});\r", + "\r", + "pm.test(\"PUT ODS Instance Derivatives Invalid: DerivativeType value allowed\", function () {\r", + " pm.expect(response.errors.DerivativeType[0]).to.contain(\"The value for the Derivative type is not allowed. The only accepted values are: 'ReadReplica' or 'Snapshot'\");\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": 0,\r\n \"derivativeType\": \"invalid\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/{{CreatedOdsInstanceDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "{{CreatedOdsInstanceDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid Connection String", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ODS Instance Derivatives Invalid: ConnectionString included\", function () {\r", + " pm.expect(response.errors).to.have.property('ConnectionString');\r", + "});\r", + "\r", + "pm.test(\"PUT ODS Instance Derivatives Invalid: ConnectionString value allowed\", function () {\r", + " pm.expect(response.errors.ConnectionString[0]).to.contain(\"The connection string is not valid.\");\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": 0,\r\n \"derivativeType\": \"Snapshot\",\r\n \"connectionString\": \"invalid-connection-string\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/{{CreatedOdsInstanceDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "{{CreatedOdsInstanceDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid ODS Instance Id", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Invalid OdsInstanceId\", function () {\r", + " pm.expect(response.errors.OdsInstanceId[0]).to.contain(\"valid ods instance id\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": 0,\r\n \"derivativeType\": \"ReadReplica\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/{{CreatedOdsInstanceDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "{{CreatedOdsInstanceDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Not Found ODS Instance Id", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Invalid OdsInstanceId\", function () {\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"odsInstance\");\r", + " pm.expect(response.title).to.contain(\"It may have been recently deleted\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": 9000,\r\n \"derivativeType\": \"ReadReplica\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/{{CreatedOdsInstanceDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "{{CreatedOdsInstanceDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid Combined Key", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstanceDerivatives/`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"odsInstanceId\": `${pm.collectionVariables.get(\"ODSInstanceId\")}`,\r", + " \"derivativeType\": \"Snapshot\",\r", + " \"connectionString\": `${pm.environment.get(\"connectionString\")}`\r", + " }), \r", + " }\r", + "},\r", + "function (error, response) {\r", + " if(error) { console.log(\"Error in Pre-request:\", error); }\r", + " const id = response.headers.get(\"Location\").split(\"/\")[2];\r", + " pm.collectionVariables.set(\"OtherOdsInstanceDerivativeId\", id);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstanceDerivatives Invalid: Invalid OdsInstanceId\", function () {\r", + " pm.expect(response.title).to.contain(\"Validation failed\");\r", + " pm.expect(response.errors).to.be.an(\"object\").and.not.to.be.empty;\r", + " pm.expect(response.errors[\"\"]).to.be.an(\"array\").that.includes(\"The combined key ODS instance id and derivative type must be unique.\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"odsInstanceId\": {{ODSInstanceId}},\r\n \"derivativeType\": \"Snapshot\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/{{CreatedOdsInstanceDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "{{CreatedOdsInstanceDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/OdsInstanceDerivatives/{{CreatedOdsInstanceDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "OdsInstanceDerivatives", + "{{CreatedOdsInstanceDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE OdsInstanceDerivatives: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/{{CreatedOdsInstanceDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "{{CreatedOdsInstanceDerivativeId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstanceDerivatives - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE OdsInstanceDerivatives NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"DELETE OdsInstanceDerivatives NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE OdsInstanceDerivatives NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"odsInstanceDerivative\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistOdsInstancesDerivativeId\"));\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/OdsInstanceDerivatives/{{NotExistOdsInstancesDerivativeId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "OdsInstanceDerivatives", + "{{NotExistOdsInstancesDerivativeId}}" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function randomIntFromInterval(min, max) { // min and max included ", + " return Math.floor(Math.random() * (max - min + 1) + min)", + "}", + "", + "const rndInt = randomIntFromInterval(450, 783)", + "pm.collectionVariables.set(\"NotExistProfileId\", pm.variables.replaceIn(rndInt));" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "NotExistProfileId", + "value": "" + }, + { + "key": "CreatedProfileId", + "value": "" + }, + { + "key": "ODSInstanceId", + "value": "" + }, + { + "key": "CreatedOdsInstanceDerivativeId", + "value": "" + }, + { + "key": "NotExistOdsInstancesDerivativeId", + "value": "90" + }, + { + "key": "CreatedOdsInstanceContextId", + "value": "" + }, + { + "key": "NotExistOdsInstancesContextId", + "value": "786" + }, + { + "key": "OtherOdsInstanceDerivativeId", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - OdsInstances.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - OdsInstances.postman_collection.json new file mode 100644 index 000000000..980a876ad --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - OdsInstances.postman_collection.json @@ -0,0 +1,1768 @@ +{ + "info": { + "_postman_id": "07006385-4935-4283-9bb0-5b58170294ae", + "name": "Admin API E2E 2.0 - OdsInstances", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "OdsInstances", + "item": [ + { + "name": "OdsInstances", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OdsInstanceGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OdsInstances: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test(\"POST OdsInstances: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " const id = pm.response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"CreatedOdsInstanceId\", id);\r", + " }\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test-OdsInstance-{{OdsInstanceGUID}}\",\r\n \"instanceType\": \"OdsInstance\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OdsInstanceGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test-OdsInstance-{{OdsInstanceGUID}}\",\r\n \"instanceType\": \"OdsInstance\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/odsInstances/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OdsInstances Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST OdsInstances Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST OdsInstances Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST OdsInstances Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ConnectionString\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST OdsInstances Invalid: Response errors include messages with wrong elements\", function () {\r", + " pm.expect(response.errors[\"Name\"][0]).to.contain(\"'Name' must not be empty\");\r", + " pm.expect(response.errors[\"ConnectionString\"][0]).to.contain(\"'Connection String' must not be empty\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"\",\r\n \"instanceType\": \"\",\r\n \"connectionString\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Invalid Existing Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OtherOdsInstanceGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"name\": `Other Ods Instance ${pm.collectionVariables.get(\"OtherOdsInstanceGUID\")}`,\r", + " \"instanceType\": \"Other Ods Instance Type\",\r", + " \"connectionString\": `${pm.environment.get(\"connectionString\")}`\r", + " }), \r", + " }\r", + "},\r", + "function (error, response) {\r", + " if(error) { console.log(\"Error in Pre-request:\", error); }\r", + " const id = response.headers.get(\"Location\").split(\"/\")[2];\r", + " pm.collectionVariables.set(\"OtherExistingOdsInstanceId\", id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OdsInstance Invalid Existing OdsInstance: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST OdsInstance Invalid Existing OdsInstance: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST OdsInstance Invalid Existing OdsInstance: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST OdsInstance Invalid Existing OdsInstance: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Name.length).to.equal(1);\r", + " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Other Ods Instance {{OtherOdsInstanceGUID}}\",\r\n \"instanceType\": \"Other Ods Instance Type\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Invalid Connection String", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OdsInstances Invalid ConnectionString: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST OdsInstances Invalid ConnectionString: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST OdsInstances Invalid ConnectionString: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST OdsInstances Invalid ConnectionString: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"ConnectionString\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST OdsInstances Invalid ConnectionString: Response errors include messages with wrong elements\", function () {\r", + " pm.expect(response.errors[\"ConnectionString\"][0]).to.contain(\"is not valid\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test Connection String\",\r\n \"instanceType\": \"OdsInstance\",\r\n \"connectionString\": \"WrongConnection\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstances: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET OdsInstances: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"instanceType\");", + "});", + "", + "const GetOdsInstancesSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"instanceType\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"instanceType\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET OdsInstances: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesSchema);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/odsInstances?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + "});", + "", + "pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/odsInstances", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Without Offset and Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstances: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET OdsInstances: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"instanceType\");", + "});", + "", + "const GetOdsInstancesSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"instanceType\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"instanceType\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET OdsInstances: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/odsInstances", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Without Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstances: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET OdsInstances: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"instanceType\");", + "});", + "", + "const GetOdsInstancesSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"instanceType\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"instanceType\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET OdsInstances: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/odsInstances?limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Without Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstances: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET OdsInstances: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"instanceType\");", + "});", + "", + "const GetOdsInstancesSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"instanceType\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"instanceType\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET OdsInstances: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/odsInstances?offset={{offset}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstances NotFound: Status code is Not Found\", function () {", + " pm.response.to.have.status(404);", + "});", + "", + "pm.test(\"GET OdsInstances NotFound: Response matches error format\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response).to.have.property(\"title\");", + "});", + "", + "pm.test(\"GET OdsInstances NotFound: Response title is helpful and accurate\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response.title).to.contain(\"Not found\");", + " pm.expect(response.title).to.contain(\"odsInstance\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/odsInstances/0", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "0" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstancesID: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET OdsInstancesID: Response matches success format\", function () {", + " pm.expect(response).to.have.property(\"id\");", + " pm.expect(response).to.have.property(\"name\");", + " pm.expect(response).to.have.property(\"instanceType\");", + " pm.expect(response).to.have.property(\"odsInstanceContexts\");", + " pm.expect(response).to.have.property(\"odsInstanceDerivatives\");", + "});", + "", + "const GetOdsInstancesIdSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"instanceType\": {", + " \"type\": \"string\"", + " },", + " \"odsInstanceContexts\": {", + " \"type\": \"array\",", + " \"items\": {}", + " },", + " \"odsInstanceDerivatives\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"instanceType\",", + " \"odsInstanceContexts\",", + " \"odsInstanceDerivatives\"", + " ]", + "}", + "", + "pm.test(\"GET OdsInstancesID: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesIdSchema);", + "});", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{CreatedOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{CreatedOdsInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances by ID Application", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstancesIDApplication: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET OdsInstancesIDApplication: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"applicationName\");", + " pm.expect(response[0]).to.have.property(\"claimSetName\");", + " pm.expect(response[0]).to.have.property(\"educationOrganizationIds\");", + " pm.expect(response[0]).to.have.property(\"vendorId\");", + " pm.expect(response[0]).to.have.property(\"profileIds\");", + " pm.expect(response[0]).to.have.property(\"odsInstanceIds\");", + "});", + "", + "const GetOdsInstancesIdApplication = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"applicationName\": {", + " \"type\": \"string\"", + " },", + " \"claimSetName\": {", + " \"type\": \"string\"", + " },", + " \"educationOrganizationIds\": {", + " \"type\": \"array\",", + " \"items\": {}", + " },", + " \"vendorId\": {", + " \"type\": \"integer\"", + " },", + " \"profileIds\": {", + " \"type\": \"array\",", + " \"items\": {}", + " },", + " \"odsInstanceIds\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"applicationName\",", + " \"claimSetName\",", + " \"educationOrganizationIds\",", + " \"vendorId\",", + " \"profileIds\",", + " \"odsInstanceIds\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET OdsInstancesIdApplication: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesIdApplication);", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json[0].id) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{ODSInstanceId}}/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{ODSInstanceId}}", + "applications" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstance: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances/${pm.collectionVariables.get(\"CreatedOdsInstanceId\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (odsInstanceErr, odsInstanceResponse) {\r", + " if(odsInstanceErr) \r", + " { \r", + " console.log(\"Error :\", odsInstanceErr); \r", + " } \r", + " const updatedOdsInstanceJson = odsInstanceResponse.json();\r", + " pm.test(\"PUT OdsInstance: Response includes updated odsInstance\", function () {\r", + " pm.expect(updatedOdsInstanceJson.name).to.equal(`Updated-Test-OdsInstance-${pm.collectionVariables.get(\"OdsInstanceGUID\")}`);\r", + " pm.expect(updatedOdsInstanceJson.instanceType).to.not.be.empty;\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Updated-Test-OdsInstance-{{OdsInstanceGUID}}\",\r\n \"instanceType\": \"Updated-Test-OdsInstanceType\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{CreatedOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{CreatedOdsInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Updated-Test-OdsInstance-{{OdsInstanceGUID}}\",\r\n \"instanceType\": \"Updated-Test-OdsInstanceType\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/odsInstances/{{CreatedOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances", + "{{CreatedOdsInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Empty Connection String", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstance connectionString empty: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances/${pm.collectionVariables.get(\"CreatedOdsInstanceId\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (odsInstanceErr, odsInstanceResponse) {\r", + " if(odsInstanceErr) \r", + " { \r", + " console.log(\"Error :\", odsInstanceErr); \r", + " } \r", + " const updatedOdsInstanceJson = odsInstanceResponse.json();\r", + " pm.test(\"PUT OdsInstance connectionString empty: Response includes updated odsInstance\", function () {\r", + " pm.expect(updatedOdsInstanceJson.name).to.equal(`Updated-Test-OdsInstance-${pm.collectionVariables.get(\"OdsInstanceGUID\")}-Empty`);\r", + " pm.expect(updatedOdsInstanceJson.instanceType).to.not.be.empty;\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Updated-Test-OdsInstance-{{OdsInstanceGUID}}-Empty\",\r\n \"instanceType\": \"Updated-Test-OdsInstanceType\",\r\n \"connectionString\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{CreatedOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{CreatedOdsInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Invalid Connection String", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstances Invalid ConnectionString: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT OdsInstances Invalid ConnectionString: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstances Invalid ConnectionString: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstances Invalid ConnectionString: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"ConnectionString\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstances Invalid ConnectionString: Response errors include messages with wrong elements\", function () {\r", + " pm.expect(response.errors[\"ConnectionString\"][0]).to.contain(\"is not valid\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Other Ods Instance {{OtherOdsInstanceGUID}}\",\r\n \"instanceType\": \"Other Ods Instance Type\",\r\n \"connectionString\": \"WrongConnectionString\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{CreatedOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{CreatedOdsInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Invalid Existing Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstance Invalid Existing OdsInstance: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT OdsInstance Invalid Existing OdsInstance: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstance Invalid Existing OdsInstance: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstance Invalid Existing OdsInstance: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Name.length).to.equal(1);\r", + " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Other Ods Instance {{OtherOdsInstanceGUID}}\",\r\n \"instanceType\": \"Other Ods Instance Type\",\r\n \"connectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{CreatedOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{CreatedOdsInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "function randomIntFromInterval(min, max) { // min and max included \r", + " return Math.floor(Math.random() * (max - min + 1) + min)\r", + "}\r", + "\r", + "const rndInt = randomIntFromInterval(99990, 99999)\r", + "pm.collectionVariables.set(\"NotExistOdsInstanceId\", pm.variables.replaceIn(rndInt));" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT OdsInstance NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstance NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT OdsInstance NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"odsInstance\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistOdsInstanceId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Updated-Test-OdsInstance\",\r\n \"instanceType\": \"Updated-Test-OdsInstanceType\",\r\n \"connectionString\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{NotExistOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{NotExistOdsInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/odsInstances/{{CreatedOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances", + "{{CreatedOdsInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE OdsInstances: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{CreatedOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{CreatedOdsInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE OdsInstance NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"DELETE OdsInstance NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE OdsInstance NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"odsInstance\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistOdsInstanceId\"));\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "function randomIntFromInterval(min, max) { // min and max included \r", + " return Math.floor(Math.random() * (max - min + 1) + min)\r", + "}\r", + "\r", + "const rndInt = randomIntFromInterval(99990, 99999)\r", + "pm.collectionVariables.set(\"NotExistOdsInstanceId\", pm.variables.replaceIn(rndInt));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{NotExistOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{NotExistOdsInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Applications associated", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE OdsInstance Applications associated: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DELETE OdsInstance Applications associated: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"DELETE OdsInstance Applications associated: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"DELETE OdsInstance Applications associated: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Id[0]).to.contain(\"Can not be deleted\");\r", + " pm.expect(response.errors.Id[0]).to.contain(\"Applications\"); \r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{ODSInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{ODSInstanceId}}" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "ODSInstanceId", + "value": "" + }, + { + "key": "NotExistOdsInstanceId", + "value": "" + }, + { + "key": "OtherClaimSetGUID", + "value": "" + }, + { + "key": "OtherExistingClaimSetId", + "value": "" + }, + { + "key": "OtherOdsInstanceGUID", + "value": "" + }, + { + "key": "OtherExistingOdsInstanceId", + "value": "" + }, + { + "key": "OdsInstanceGUID", + "value": "" + }, + { + "key": "CreatedOdsInstanceId", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Profiles.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Profiles.postman_collection.json new file mode 100644 index 000000000..5ae488657 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Profiles.postman_collection.json @@ -0,0 +1,1672 @@ +{ + "info": { + "_postman_id": "2ed8d3c4-c857-404b-960c-6b0553f3a164", + "name": "Admin API E2E 2.0 - Profiles", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "Profiles", + "item": [ + { + "name": "Profiles", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " const id = pm.response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"CreatedProfileId\", id);\r", + " }\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/Profiles/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "Profiles", + "" + ] + } + }, + "response": [] + }, + { + "name": "Profiles Duplicate Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Name\"][0].toLowerCase()).to.contain(\"a profile with this name already exists in the database. please enter a unique name.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Name\"][0].toLowerCase()).to.contain(\"'name' must not be empty\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Invalid definition xml", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Definition\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"test\");\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"readcontenttype\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Name mismatch", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Definition\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"match with test-profile-123\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile-123\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ] + } + }, + "response": [] + }, + { + "name": "Profiles", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profiles: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result includes profiles\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetProfilesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Profile: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetProfilesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/Profiles/?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/Profiles", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "Profiles" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Without Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profiles: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result includes profiles\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetProfilesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Profile: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetProfilesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/Profiles/?limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ], + "query": [ + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Without Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profiles: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result includes profiles\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetProfilesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Profile: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetProfilesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/Profiles/?offset={{offset}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Without Offset and Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profiles: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result includes profiles\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetProfilesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Profile: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetProfilesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/Profiles/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ] + } + }, + "response": [] + }, + { + "name": "Profile by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ProfileById: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET ProfileById: Response result matches profile\", function () {\r", + " const result = pm.response.json();\r", + " pm.expect(result.name).to.equal(`Test-Profile`);\r", + " pm.expect(result.definition).to.not.be.empty; \r", + "});\r", + "\r", + "const GetProfileIdSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"definition\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " }\r", + "}\r", + "\r", + "pm.test(\"GET ProfileById: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetProfileIdSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profile by ID - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profile NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"profile\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistProfileId\"));\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/profiles/{{NotExistProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles", + "{{NotExistProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles Duplicate Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/Profiles`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"Name\": \"Other-Test-Profile\",\r", + " \"Definition\": \"\"\r", + " }), \r", + " }\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const id = response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"OtherCreatedProfileId\", id);\r", + " }\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Profiles: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Name\"][0].toLowerCase()).to.contain(\"another profile with this name already exists in the database. please enter a unique name.\");\r", + "});\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/profiles/${pm.collectionVariables.get(\"OtherCreatedProfileId\")}`,\r", + " method: 'DELETE',\r", + " header: header\r", + "},\r", + "function (err, profileResponse) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/{{OtherCreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "{{OtherCreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Invalid Api Mode", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Updated-Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/Profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "Profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Profiles: Status code is Created\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/profiles/${pm.collectionVariables.get(\"CreatedProfileId\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (profileErr, profileResponse) {\r", + " if(profileErr) \r", + " { \r", + " console.log(\"Error :\", profileErr); \r", + " } \r", + " const updatedProfileJson = profileResponse.json();\r", + " pm.test(\"PUT Profiles: Response includes updated profile\", function () {\r", + " pm.expect(updatedProfileJson.name).to.equal(\"Updated-Test-Profile\");\r", + " pm.expect(updatedProfileJson.definition).to.not.be.empty;\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Updated-Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profile NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"profile\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistProfileId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Not-Found\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/profiles/{{NotExistProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles", + "{{NotExistProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Name\"][0].toLowerCase()).to.contain(\"'name' must not be empty\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Invalid definition xml", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Definition\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"test\");\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"readcontenttype\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Updated-Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Name mismatch", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Definition\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"match with test-profile-123\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile-123\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE Profile: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profile NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"profile\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistProfileId\"));\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/profiles/{{NotExistProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles", + "{{NotExistProfileId}}" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function randomIntFromInterval(min, max) { // min and max included ", + " return Math.floor(Math.random() * (max - min + 1) + min)", + "}", + "", + "const rndInt = randomIntFromInterval(450, 783)", + "pm.collectionVariables.set(\"NotExistProfileId\", pm.variables.replaceIn(rndInt));" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "NotExistProfileId", + "value": "" + }, + { + "key": "CreatedProfileId", + "value": "" + }, + { + "key": "OtherCreatedProfileId", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - ResourceClaimActionAuthStrategies.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - ResourceClaimActionAuthStrategies.postman_collection.json new file mode 100644 index 000000000..c296afa33 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - ResourceClaimActionAuthStrategies.postman_collection.json @@ -0,0 +1,850 @@ +{ + "info": { + "_postman_id": "1bb3ec3a-88b7-4987-a815-4a34806b3b65", + "name": "Admin API E2E 2.0 - ResourceClaimActionAuthStrategies", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "ResourceClaimActionAuthStrategies", + "item": [ + { + "name": "ResourceClaimActionAuthStrategies", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimActionAuthStrategies: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "const GetResourceClaimActionAuthStrategiesActionsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resourceClaimId\": {", + " \"type\": \"integer\"", + " },", + " \"resourceName\": {", + " \"type\": \"string\"", + " },", + " \"claimName\" : {", + " \"type\": \"string\"", + " },", + " \"authorizationStrategiesForActions\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"actionId\": {", + " \"type\": \"integer\"", + " },", + " \"actionName\": {", + " \"type\": \"string\"", + " },", + " \"authorizationStrategies\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"authStrategyId\": {", + " \"type\": \"integer\"", + " },", + " \"authStrategyName\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"authStrategyId\",", + " \"authStrategyName\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"actionId\",", + " \"actionName\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"resourceClaimId\",", + " \"resourceName\",", + " \"claimName\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimActionAuthStrategies: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimActionAuthStrategiesActionsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaimActionAuthStrategies?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaimActionAuthStrategies" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimActionAuthStrategies - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + "});", + "", + "pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/resourceClaimActionAuthStrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "resourceClaimActionAuthStrategies" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimActionAuthStrategies - Without Limit and Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimActionAuthStrategies: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "const GetResourceClaimActionAuthStrategiesActionsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resourceClaimId\": {", + " \"type\": \"integer\"", + " },", + " \"resourceName\": {", + " \"type\": \"string\"", + " },", + " \"claimName\" : {", + " \"type\": \"string\"", + " },", + " \"authorizationStrategiesForActions\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"actionId\": {", + " \"type\": \"integer\"", + " },", + " \"actionName\": {", + " \"type\": \"string\"", + " },", + " \"authorizationStrategies\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"authStrategyId\": {", + " \"type\": \"integer\"", + " },", + " \"authStrategyName\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"authStrategyId\",", + " \"authStrategyName\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"actionId\",", + " \"actionName\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"resourceClaimId\",", + " \"resourceName\",", + " \"claimName\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimActionAuthStrategies: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimActionAuthStrategiesActionsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaimActionAuthStrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaimActionAuthStrategies" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimActionAuthStrategies - Without Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimActionAuthStrategies: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "const GetResourceClaimActionAuthStrategiesActionsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resourceClaimId\": {", + " \"type\": \"integer\"", + " },", + " \"resourceName\": {", + " \"type\": \"string\"", + " },", + " \"claimName\" : {", + " \"type\": \"string\"", + " },", + " \"authorizationStrategiesForActions\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"actionId\": {", + " \"type\": \"integer\"", + " },", + " \"actionName\": {", + " \"type\": \"string\"", + " },", + " \"authorizationStrategies\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"authStrategyId\": {", + " \"type\": \"integer\"", + " },", + " \"authStrategyName\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"authStrategyId\",", + " \"authStrategyName\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"actionId\",", + " \"actionName\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"resourceClaimId\",", + " \"resourceName\",", + " \"claimName\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimActionAuthStrategies: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimActionAuthStrategiesActionsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaimActionAuthStrategies?limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaimActionAuthStrategies" + ], + "query": [ + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimActionAuthStrategies - Without Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimActionAuthStrategies: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "const GetResourceClaimActionAuthStrategiesActionsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resourceClaimId\": {", + " \"type\": \"integer\"", + " },", + " \"resourceName\": {", + " \"type\": \"string\"", + " },", + " \"claimName\" : {", + " \"type\": \"string\"", + " },", + " \"authorizationStrategiesForActions\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"actionId\": {", + " \"type\": \"integer\"", + " },", + " \"actionName\": {", + " \"type\": \"string\"", + " },", + " \"authorizationStrategies\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"authStrategyId\": {", + " \"type\": \"integer\"", + " },", + " \"authStrategyName\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"authStrategyId\",", + " \"authStrategyName\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"actionId\",", + " \"actionName\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"resourceClaimId\",", + " \"resourceName\",", + " \"claimName\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimActionAuthStrategies: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimActionAuthStrategiesActionsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaimActionAuthStrategies?offset={{offset}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaimActionAuthStrategies" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimActionAuthStrategies - Filter by resourceName", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimActionAuthStrategies: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "const GetResourceClaimActionAuthStrategiesActionsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resourceClaimId\": {", + " \"type\": \"integer\"", + " },", + " \"resourceName\": {", + " \"type\": \"string\"", + " },", + " \"claimName\" : {", + " \"type\": \"string\"", + " },", + " \"authorizationStrategiesForActions\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"actionId\": {", + " \"type\": \"integer\"", + " },", + " \"actionName\": {", + " \"type\": \"string\"", + " },", + " \"authorizationStrategies\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"authStrategyId\": {", + " \"type\": \"integer\"", + " },", + " \"authStrategyName\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"authStrategyId\",", + " \"authStrategyName\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"actionId\",", + " \"actionName\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"resourceClaimId\",", + " \"resourceName\",", + " \"claimName\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimActionAuthStrategies: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimActionAuthStrategiesActionsSchema);", + "});", + "", + "pm.test(\"GET ResourceClaimActionAuthStrategies: Response result includes resource claim action\", function () {", + " const results = pm.response.json();", + " pm.expect(results).to.have.lengthOf(1);", + "});", + "", + "pm.test(\"GET ResourceClaimActionAuthStrategies: Response result contains requested resource claim action\", function () {", + " const results = pm.response.json();", + " pm.expect(results[0].resourceName).to.eql(pm.collectionVariables.get(\"RESOURCENAMEFILTER\"));", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaimActionAuthStrategies?resourceName={{RESOURCENAMEFILTER}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaimActionAuthStrategies" + ], + "query": [ + { + "key": "resourceName", + "value": "{{RESOURCENAMEFILTER}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimActionAuthStrategies - Filter by non-existing resourceName", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimActionAuthStrategies: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "const GetResourceClaimActionAuthStrategiesActionsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resourceClaimId\": {", + " \"type\": \"integer\"", + " },", + " \"resourceName\": {", + " \"type\": \"string\"", + " },", + " \"claimName\" : {", + " \"type\": \"string\"", + " },", + " \"authorizationStrategiesForActions\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"actionId\": {", + " \"type\": \"integer\"", + " },", + " \"actionName\": {", + " \"type\": \"string\"", + " },", + " \"authorizationStrategies\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"authStrategyId\": {", + " \"type\": \"integer\"", + " },", + " \"authStrategyName\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"authStrategyId\",", + " \"authStrategyName\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"actionId\",", + " \"actionName\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"resourceClaimId\",", + " \"resourceName\",", + " \"claimName\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimActionAuthStrategies: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimActionAuthStrategiesActionsSchema);", + "});", + "", + "pm.test(\"GET ResourceClaimActionAuthStrategies: Response result not includes resource claim action\", function () {", + " const results = pm.response.json();", + " pm.expect(results).to.have.lengthOf(0);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaimActionAuthStrategies?resourceName=1234", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaimActionAuthStrategies" + ], + "query": [ + { + "key": "resourceName", + "value": "1234" + } + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "RESOURCENAMEFILTER", + "value": "candidate", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Resourceclaims.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Resourceclaims.postman_collection.json new file mode 100644 index 000000000..ea2894f50 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Resourceclaims.postman_collection.json @@ -0,0 +1,1244 @@ +{ + "info": { + "_postman_id": "2178e3d3-1a70-4309-b58b-aa74231623e1", + "name": "Admin API E2E 2.0 - Resourceclaims", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "Resourceclaims", + "item": [ + { + "name": "Resourceclaims", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaims: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET ResourceClaims: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"parentId\");", + " pm.expect(response[0]).to.have.property(\"parentName\");", + " pm.expect(response[0]).to.have.property(\"children\");", + "});", + "", + "const GetResourceClaimsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"null\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaims: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsSchema);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaims?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaims" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + "});", + "", + "pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/resourceClaims", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "resourceClaims" + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims - Without Limit and Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaims: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET ResourceClaims: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"parentId\");", + " pm.expect(response[0]).to.have.property(\"parentName\");", + " pm.expect(response[0]).to.have.property(\"children\");", + "});", + "", + "const GetResourceClaimsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"null\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaims: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaims", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaims" + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims - Without Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaims: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET ResourceClaims: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"parentId\");", + " pm.expect(response[0]).to.have.property(\"parentName\");", + " pm.expect(response[0]).to.have.property(\"children\");", + "});", + "", + "const GetResourceClaimsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"null\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaims: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaims?limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaims" + ], + "query": [ + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims - Without Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaims: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET ResourceClaims: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"parentId\");", + " pm.expect(response[0]).to.have.property(\"parentName\");", + " pm.expect(response[0]).to.have.property(\"children\");", + "});", + "", + "const GetResourceClaimsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"null\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaims: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaims?offset={{offset}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaims" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimsId: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET ResourceClaimsId: Response matches success format\", function () {", + " pm.expect(response).to.have.property(\"id\");", + " pm.expect(response).to.have.property(\"name\");", + " pm.expect(response).to.have.property(\"parentId\");", + " pm.expect(response).to.have.property(\"parentName\");", + " pm.expect(response).to.have.property(\"children\");", + "});", + "", + "const GetResourceClaimsIdSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"null\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimsId: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsIdSchema);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaims/1", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaims", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims -Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Resourceclaim NotFound: Status code is Not Found\", function () {", + " pm.response.to.have.status(404);", + "});", + "", + "pm.test(\"GET Resourceclaim NotFound: Response matches error format\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response).to.have.property(\"title\");", + "});", + "", + "pm.test(\"GET Resourceclaim NotFound: Response title is helpful and accurate\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response.title).to.contain(\"Not found\");", + " pm.expect(response.title).to.contain(\"resourceclaim\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaims/00", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaims", + "00" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - ResourceclaimsActions.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - ResourceclaimsActions.postman_collection.json new file mode 100644 index 000000000..ca78edde3 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - ResourceclaimsActions.postman_collection.json @@ -0,0 +1,718 @@ +{ + "info": { + "_postman_id": "b0aace15-4437-4be9-a505-496c545ee500", + "name": "Admin API E2E 2.0 - ResourceClaimsActions", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "ResourceClaimsActions", + "item": [ + { + "name": "ResourceClaimsActions", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaims: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "const GetResourceClaimsActionsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resourceClaimId\": {", + " \"type\": \"integer\"", + " },", + " \"resourceName\": {", + " \"type\": \"string\"", + " },", + " \"claimName\": {", + " \"type\": \"string\"", + " },", + " \"actions\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"name\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"resourceClaimId\",", + " \"resourceName\",", + " \"claimName\",", + " \"actions\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimsActions: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsActionsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaimActions?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaimActions" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimsActions - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + "});", + "", + "pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/resourceClaimActions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "resourceClaimActions" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimsActions - Without Limit and Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaims: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "const GetResourceClaimsActionsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resourceClaimId\": {", + " \"type\": \"integer\"", + " },", + " \"resourceName\": {", + " \"type\": \"string\"", + " },", + " \"claimName\": {", + " \"type\": \"string\"", + " },", + " \"actions\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"name\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"resourceClaimId\",", + " \"resourceName\",", + " \"claimName\",", + " \"actions\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimsActions: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsActionsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaimActions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaimActions" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimsActions - Without Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaims: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "const GetResourceClaimsActionsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resourceClaimId\": {", + " \"type\": \"integer\"", + " },", + " \"resourceName\": {", + " \"type\": \"string\"", + " },", + " \"claimName\": {", + " \"type\": \"string\"", + " },", + " \"actions\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"name\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"resourceClaimId\",", + " \"resourceName\",", + " \"claimName\",", + " \"actions\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimsActions: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsActionsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaimActions?limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaimActions" + ], + "query": [ + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimsActions - Without Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaims: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "const GetResourceClaimsActionsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resourceClaimId\": {", + " \"type\": \"integer\"", + " },", + " \"resourceName\": {", + " \"type\": \"string\"", + " },", + " \"claimName\": {", + " \"type\": \"string\"", + " },", + " \"actions\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"name\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"resourceClaimId\",", + " \"resourceName\",", + " \"claimName\",", + " \"actions\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimsActions: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsActionsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaimActions?offset={{offset}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaimActions" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimsActions - Filter by ResourceName", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimsActions: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "const GetResourceClaimsActionsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resourceClaimId\": {", + " \"type\": \"integer\"", + " },", + " \"resourceName\": {", + " \"type\": \"string\"", + " },", + " \"claimName\": {", + " \"type\": \"string\"", + " },", + " \"actions\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"name\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"resourceClaimId\",", + " \"resourceName\",", + " \"claimName\",", + " \"actions\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimsActions: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsActionsSchema);", + "});", + "", + "pm.test(\"GET ResourceClaimsActions: Response result includes resource claim action\", function () {", + " const results = pm.response.json();", + " pm.expect(results).to.have.lengthOf(1);", + "});", + "", + "pm.test(\"GET ResourceClaimsActions: Response result contains requested resource claim action\", function () {", + " const results = pm.response.json();", + " pm.expect(results[0].resourceName).to.eql(pm.collectionVariables.get(\"RESOURCENAMEFILTER\"));", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaimActions?resourceName={{RESOURCENAMEFILTER}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaimActions" + ], + "query": [ + { + "key": "resourceName", + "value": "{{RESOURCENAMEFILTER}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimsActions - Filter by non-existing ResourceName", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimsActions: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "const GetResourceClaimsActionsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resourceClaimId\": {", + " \"type\": \"integer\"", + " },", + " \"resourceName\": {", + " \"type\": \"string\"", + " },", + " \"claimName\": {", + " \"type\": \"string\"", + " },", + " \"actions\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"name\"", + " ]", + " }]", + " }", + " },", + " \"required\": [", + " \"resourceClaimId\",", + " \"resourceName\",", + " \"claimName\",", + " \"actions\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimsActions: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsActionsSchema);", + "});", + "", + "pm.test(\"GET ResourceClaimsActions: Response result not includes resource claim action\", function () {", + " const results = pm.response.json();", + " pm.expect(results).to.have.lengthOf(0);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceClaimActions?resourceName=1234", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceClaimActions" + ], + "query": [ + { + "key": "resourceName", + "value": "1234" + } + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "RESOURCENAMEFILTER", + "value": "candidate", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - Application.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - Application.postman_collection.json new file mode 100644 index 000000000..6ac38cf3c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - Application.postman_collection.json @@ -0,0 +1,1159 @@ +{ + "info": { + "_postman_id": "466e196e-c944-44cc-9ebe-f9d42517a87b", + "name": "Admin API E2E 2.0 - Sorting and Filtering - Application", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "37902114" + }, + "item": [ + { + "name": "Get Applications order by Default Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result is ordered by ApplicationName asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [application => application.applicationName],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Applications order by Default Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result is ordered by ApplicationName desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [application => application.applicationName],['desc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}&direction=DESC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "direction", + "value": "DESC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Applications order by Id Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result is ordered by Id asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [application => application.id],['asc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}&orderBy=Id&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "Id" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Applications order by Id Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result is ordered by Id desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [application => application.id],['desc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}&orderBy=Id&direction=DESC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "Id" + }, + { + "key": "direction", + "value": "DESC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Applications order by ApplicationName Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result is ordered by ApplicationName asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [application => application.applicationName],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}&orderBy=applicationName&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "applicationName" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Applications order by ApplicationName Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result is ordered by ApplicationName desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [application => application.applicationName],['desc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}&orderBy=applicationName&direction=DESC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "applicationName" + }, + { + "key": "direction", + "value": "DESC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Applications order by ClaimsetName Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result is ordered by ClaimsetName asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [application => application.claimSetName],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}&orderBy=claimsetName&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "claimsetName" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Applications order by ClaimsetName Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result is ordered by ClaimSetName Name desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [application => application.claimSetName],['desc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}&orderBy=claimsetName&direction=DESC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "claimsetName" + }, + { + "key": "direction", + "value": "DESC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Applications filter by Application Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result includes application\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results).to.have.lengthOf(1);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result contains requested application\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results[0].applicationName).to.eql(pm.collectionVariables.get(\"FILTERAPPLICATIONNAME\"));\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}&applicationName={{FILTERAPPLICATIONNAME}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "applicationName", + "value": "{{FILTERAPPLICATIONNAME}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Applications filter by Claimset Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result includes application\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results).to.have.lengthOf(1);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response result contains requested application\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results[0].applicationName).to.eql(pm.collectionVariables.get(\"FILTERAPPLICATIONNAME\"));\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}&claimsetName={{FILTERCLAIMSETNAME}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "claimsetName", + "value": "{{FILTERCLAIMSETNAME}}" + } + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "\r", + "/// -------\r", + "/// Authentication\r", + "/// -------\r", + "\r", + "const jsHelperVar = pm.variables.get('jsHelper');\r", + "const jsHelper = eval(jsHelperVar);\r", + "\r", + "var header = {\r", + " 'Content-Type': 'application/x-www-form-urlencoded'\r", + "};\r", + "\r", + "var guid = pm.variables.replaceIn('{{$guid}}');\r", + "var client_secret = jsHelper().generateClientSecret();\r", + "\r", + " const registerRequest = {\r", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'urlencoded',\r", + " urlencoded: [\r", + " {key: 'ClientId', value: guid },\r", + " {key: 'ClientSecret', value: client_secret },\r", + " {key: 'DisplayName', value: guid }\r", + " ]\r", + " }\r", + " }\r", + "\r", + " const tokenRequest = {\r", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'urlencoded',\r", + " urlencoded: [\r", + " {key: 'client_id', value: guid },\r", + " {key: 'client_secret', value: client_secret },\r", + " {key: 'grant_type', value: \"client_credentials\"},\r", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}\r", + " ]\r", + " }\r", + " }\r", + "\r", + " if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + " }\r", + "\r", + "/// -------\r", + "/// Vendors\r", + "/// -------\r", + "\r", + "function generateAlphanumericString() {\r", + " const minLength = 8;\r", + " const maxLength = 24;\r", + " let result = '';\r", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\r", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;\r", + "\r", + " result += jsHelper().randomChar('abcdefghijklmnopqrstuvwxyz');\r", + " result += jsHelper().randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');\r", + " result += jsHelper().randomChar('0123456789');\r", + "\r", + " for (let i = result.length; i < length; i++) {\r", + " result += characters.charAt(Math.floor(Math.random() * characters.length));\r", + " }\r", + "\r", + " return jsHelper().shuffleString(result);\r", + "}\r", + "\r", + "pm.sendRequest(registerRequest,\r", + " (err, res) => {\r", + " let error = res.json().error\r", + " if(error) {\r", + " throw res.json().error_description\r", + " }\r", + "\r", + " pm.sendRequest(tokenRequest,\r", + " (err, res) => {\r", + " let error = res.json().error\r", + " if(error) {\r", + " throw res.json().error_description\r", + " }\r", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)\r", + "\r", + " header = {\r", + " 'Content-Type': 'application/json',\r", + " 'Authorization': `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " };\r", + " \r", + " if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + " }\r", + " let totalToCreate = parseInt(pm.collectionVariables.get(\"APPLICATIONCOUNT\"), 10);\r", + " var claimsetsToDelete = [];\r", + " var vendorsToDelete = [];\r", + " var odsintancesToDelete = [];\r", + " var applicationsToDelete = [];\r", + "\r", + " for (let i = 0; i < totalToCreate; i++) {\r", + " let applicationName = generateAlphanumericString();\r", + " let claimsetName = generateAlphanumericString();\r", + " var vendorId = 0;\r", + " var odsInstanceId = 0;\r", + " pm.collectionVariables.set(\"FILTERAPPLICATIONNAME\", applicationName);\r", + " pm.collectionVariables.set(\"FILTERCLAIMSETNAME\", claimsetName);\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({ \"name\": `${claimsetName}` })\r", + "\r", + " }\r", + " }, (err, res) => {\r", + " let location = res.headers.get('Location');\r", + " let matches = location.match(/(\\d+)/);\r", + " id = parseInt(matches[0], 10);\r", + " claimsetsToDelete.push(id)\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({ \"company\": generateAlphanumericString(), \"namespacePrefixes\": `uri://${generateAlphanumericString()}.org`, \"contactName\": generateAlphanumericString(), \"contactEmailAddress\": `${generateAlphanumericString()}@test-ed-fi.org` })\r", + " }\r", + " }, (err, res) => {\r", + " let location = res.headers.get('Location');\r", + " let matches = location.match(/(\\d+)/);\r", + " id = parseInt(matches[0], 10);\r", + " vendorId = id;\r", + " vendorsToDelete.push(id)\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({ \"name\": generateAlphanumericString(), \"instanceType\": 'postgresql', \"connectionString\": pm.variables.get(\"connectionString\") })\r", + " }\r", + " }, (err, res) => {\r", + " let location = res.headers.get('Location');\r", + " let matches = location.match(/(\\d+)/);\r", + " id = parseInt(matches[0], 10);\r", + " odsInstanceId = id;\r", + " odsintancesToDelete.push(id)\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/applications`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({ \"applicationName\": applicationName, \"vendorId\": vendorId, \"claimSetName\": claimsetName, 'educationOrganizationIds': [ 0 ], \"odsInstanceIds\": [ odsInstanceId ]})\r", + " }\r", + " }, (err, res) => {\r", + " let location = res.headers.get('location');\r", + " let matches = location.match(/(\\d+)/);\r", + " let id = parseInt(matches[0], 10);\r", + " applicationsToDelete.push(id)\r", + " });\r", + " });\r", + " });\r", + " });\r", + " }\r", + " pm.collectionVariables.set(\"VENDORTODELETE\", vendorsToDelete);\r", + " pm.collectionVariables.set(\"CLAIMSETSTODELETE\", claimsetsToDelete);\r", + " pm.collectionVariables.set(\"ODSINSTANCETODELETE\", odsintancesToDelete);\r", + " pm.collectionVariables.set(\"APPLICATIONSTODELETE\", applicationsToDelete);\r", + " });\r", + " });\r", + "\r", + "\r", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "header = {\r", + " 'Content-Type': 'application/json',\r", + " 'Accept': '*/*',\r", + " 'Authorization': `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + "}\r", + "\r", + "const deleteVendorById = (id) => new Promise((resolve) => {\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors/${id}`,\r", + " method: 'DELETE',\r", + " header: header\r", + " }, function (err, res) {\r", + " resolve();\r", + " });\r", + "});\r", + "\r", + "function deleteClaimsetById(id) {\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets/${id}`,\r", + " method: 'DELETE',\r", + " header: header\r", + " }, function (err, res) {\r", + " if (err) {\r", + " console.log(err);\r", + " }\r", + " });\r", + "}\r", + "\r", + "function deleteODSInstanceById(id) {\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances/${id}`,\r", + " method: 'DELETE',\r", + " header: header\r", + " }, function (err, res) {\r", + " if (err) {\r", + " console.log(err);\r", + " }\r", + " });\r", + "}\r", + "\r", + "function deleteClaimsets() {\r", + " let claimsetsToDelete = pm.collectionVariables.get(\"CLAIMSETSTODELETE\").split(\",\");\r", + " for (let i = 0; i < claimsetsToDelete.length; i++) {\r", + " deleteClaimsetById(claimsetsToDelete[i]);\r", + " }\r", + "}\r", + "\r", + "function deleteOdsInstance() {\r", + " let odsInstanceToDelete = pm.collectionVariables.get(\"ODSINSTANCETODELETE\").split(\",\");\r", + " for (let i = 0; i < odsInstanceToDelete.length; i++) {\r", + " deleteODSInstanceById(odsInstanceToDelete);\r", + " }\r", + "}\r", + "\r", + "(async function () {\r", + " let vendorToDelete = pm.collectionVariables.get(\"VENDORTODELETE\").split(\",\");\r", + " for (let i = 0; i < vendorToDelete.length; i++) {\r", + " await deleteVendorById(vendorToDelete[i]);\r", + " }\r", + " deleteClaimsets();\r", + " deleteOdsInstance();\r", + "})();" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "", + "type": "string" + }, + { + "key": "FILTERAPPLICATIONNAME", + "value": "" + }, + { + "key": "FILTERCLAIMSETNAME", + "value": "" + }, + { + "key": "APPLICATIONCOUNT", + "value": "4", + "type": "string" + }, + { + "key": "VENDORTODELETE", + "value": "", + "type": "string" + }, + { + "key": "CLAIMSETSTODELETE", + "value": "", + "type": "string" + }, + { + "key": "ODSINSTANCETODELETE", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - Claimset.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - Claimset.postman_collection.json new file mode 100644 index 000000000..bd3ca90fe --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - Claimset.postman_collection.json @@ -0,0 +1,800 @@ +{ + "info": { + "_postman_id": "e2786e24-07c1-457f-8a18-718ab118ccf4", + "name": "Admin API E2E 2.0 - Sorting and Filtering - Claimset", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "37902114" + }, + "item": [ + { + "name": "Get Claimsets order by Default ASC", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Claimsets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result includes claimsets\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result is ordered by Name asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [claimset => claimset.name],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}&limit={{limit}}&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Claimsets order by Default Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Claimsets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result includes claimsets\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result is ordered by Name asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [claimset => claimset.name],['desc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}&limit={{limit}}&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Claimsets order by Id ASC", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Claimsets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result includes claimsets\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result is ordered by Id asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [claimset => claimset.id],['asc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}&limit={{limit}}&orderBy=Id&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "Id" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Claimsets order by Id Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Claimsets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result includes claimsets\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result is ordered by Id asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [claimset => claimset.id],['desc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}&limit={{limit}}&orderBy=Id&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "Id" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Claimsets order by Name ASC", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Claimsets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result includes claimsets\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result is ordered by Name asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [claimset => claimset.name],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}&limit={{limit}}&orderBy=name&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "name" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Claimsets order by Name Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Claimsets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result includes claimsets\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result is ordered by Name asc\", function () {\r", + " const results = pm.response.json();\r", + " // var expectedSortedOrder = _.orderBy(results, [claimset => claimset.name.toLowerCase()],['desc']);\r", + " const strSortByProperty = pm.variables.get('sortByProperty');\r", + " eval(strSortByProperty);\r", + " let expectedSortedOrder = sortByProperty(results, 'name', -1);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}&limit={{limit}}&orderBy=Name&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "Name" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Claimsets filter by Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Claimsets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result includes claimset\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results).to.have.lengthOf(1);\r", + "});\r", + "\r", + "pm.test(\"GET Claimsets: Response result contains requested claimset\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results[0].name).to.eql(pm.collectionVariables.get(\"FILTERNAME\"));\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}&limit={{limit}}&name={{FILTERNAME}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "name", + "value": "{{FILTERNAME}}" + } + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "/// -------\r", + "/// Authentication\r", + "/// -------\r", + "const jsHelperVar = pm.variables.get('jsHelper');\r", + "const jsHelper = eval(jsHelperVar);\r", + "\r", + "var header = {\r", + " 'Content-Type': 'application/x-www-form-urlencoded'\r", + "};\r", + "\r", + "var guid = pm.variables.replaceIn('{{$guid}}');\r", + "var client_secret = jsHelper().generateClientSecret();\r", + "\r", + " const registerRequest = {\r", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'urlencoded',\r", + " urlencoded: [\r", + " {key: 'ClientId', value: guid },\r", + " {key: 'ClientSecret', value: client_secret },\r", + " {key: 'DisplayName', value: guid }\r", + " ]\r", + " }\r", + " }\r", + "\r", + " const tokenRequest = {\r", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'urlencoded',\r", + " urlencoded: [\r", + " {key: 'client_id', value: guid },\r", + " {key: 'client_secret', value: client_secret },\r", + " {key: 'grant_type', value: \"client_credentials\"},\r", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}\r", + " ]\r", + " }\r", + " }\r", + "\r", + " if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + " }\r", + "\r", + "/// -------\r", + "/// Claimsets\r", + "/// -------\r", + "\r", + "function generateAlphanumericString() {\r", + " const minLength = 8;\r", + " const maxLength = 24;\r", + " let result = '';\r", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\r", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;\r", + "\r", + " result += jsHelper().randomChar('abcdefghijklmnopqrstuvwxyz');\r", + " result += jsHelper().randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');\r", + " result += jsHelper().randomChar('0123456789');\r", + "\r", + " for (let i = result.length; i < length; i++) {\r", + " result += characters.charAt(Math.floor(Math.random() * characters.length));\r", + " }\r", + "\r", + " return jsHelper().shuffleString(result);\r", + "}\r", + "\r", + "pm.sendRequest(registerRequest,\r", + " (err, res) => {\r", + " let error = res.json().error\r", + " if(error) {\r", + " throw res.json().error_description\r", + " }\r", + "\r", + " pm.sendRequest(tokenRequest,\r", + " (err, res) => {\r", + " let error = res.json().error\r", + " if(error) {\r", + " throw res.json().error_description\r", + " }\r", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)\r", + "\r", + " header = {\r", + " 'Content-Type': 'application/json',\r", + " 'Authorization': `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " };\r", + " \r", + " if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + " }\r", + " let totalToCreate = parseInt(pm.collectionVariables.get(\"CLAIMSETCOUNT\"), 10);\r", + " var claimsetsToDelete = [];\r", + " for (let i = 0; i < totalToCreate; i++) {\r", + " let name = generateAlphanumericString();\r", + " pm.collectionVariables.set(\"FILTERNAME\", name);\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({ \"name\": `${name}` })\r", + "\r", + " }\r", + " }, (err, res) => {\r", + " let location = res.headers.get('Location');\r", + " let matches = location.match(/(\\d+)/);\r", + " id = parseInt(matches[0], 10);\r", + " claimsetsToDelete.push(id)\r", + " });\r", + " }\r", + " pm.collectionVariables.set(\"CLAIMSETSTODELETE\", claimsetsToDelete);\r", + " });\r", + " });\r", + "\r", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "var header = {\r", + " 'Content-Type': 'application/json',\r", + " 'Accept': '*/*',\r", + " 'Authorization': `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " };\r", + " if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + " }\r", + " function deleteClaimsetById(id) {\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimSets/${id}`,\r", + " method: 'DELETE',\r", + " header: header\r", + " }, function (err, res) {\r", + " if (err) {\r", + " console.log(err);\r", + " }\r", + " });\r", + " }\r", + " function deleteClaimsets() {\r", + " let claimsetsToDelete = pm.collectionVariables.get(\"CLAIMSETSTODELETE\").split(\",\");\r", + " for (let i = 0; i < claimsetsToDelete.length; i++) {\r", + " deleteClaimsetById(claimsetsToDelete[i]);\r", + " }\r", + " }\r", + " deleteClaimsets();\r", + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "", + "type": "string" + }, + { + "key": "FILTERNAME", + "value": "", + "type": "string" + }, + { + "key": "CLAIMSETCOUNT", + "value": "4", + "type": "string" + }, + { + "key": "CLAIMSETSTODELETE", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - ODS Instances.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - ODS Instances.postman_collection.json new file mode 100644 index 000000000..7b6f28f78 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - ODS Instances.postman_collection.json @@ -0,0 +1,1060 @@ +{ + "info": { + "_postman_id": "be57956c-6278-4137-8514-55c0d27a5f3e", + "name": "Admin API E2E 2.0 - Sorting and Filtering - ODS Instances", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "37902114" + }, + "item": [ + { + "name": "Get ODS Instances order by Default Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET ODS Instances: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result includes ODS Instances\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result is ordered by Name asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [dt => dt.name],['asc']) \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances?offset={{offset}}&limit={{limit}}&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get ODS Instances order by Default Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET ODS Instances: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result includes ODS Instances\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result is ordered by Namne desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [odsInstance => odsInstance.name],['desc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances?offset={{offset}}&limit={{limit}}&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get ODS Instances order by Id Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET ODS Instances: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result includes ODS Instances\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result is ordered by Id asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [odsInstance => odsInstance.id],['asc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances?offset={{offset}}&limit={{limit}}&orderBy=id&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "id" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get ODS Instances order by Id Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET ODS Instances: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result includes ODS Instances\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result is ordered by Id desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [odsInstance => odsInstance.id],['desc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances?offset={{offset}}&limit={{limit}}&orderBy=id&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "id" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get ODS Instances order by Name Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET ODS Instances: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result includes ODS Instances\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result is ordered by Name asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [odsInstance => odsInstance.name],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances?offset={{offset}}&limit={{limit}}&orderBy=name&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "name" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get ODS Instances order by Name Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET ODS Instances: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result includes ODS Instances\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result is ordered by Name desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [odsInstance => odsInstance.name],['desc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances?offset={{offset}}&limit={{limit}}&orderBy=Name&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "Name" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get ODS Instances order by InstanceType Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET ODS Instances: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result includes ODS Instances\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result is ordered by InstanceType asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [odsInstance => odsInstance.instanceType ? odsInstance.instanceType : null],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances?offset={{offset}}&limit={{limit}}&orderBy=instanceType&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "instanceType" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get ODS Instances order by InstanceType Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET ODS Instances: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result includes ODS Instances\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET ODS Instances: Response result is ordered by InstanceType desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [odsInstance => odsInstance.instanceType ? odsInstance.instanceType : null],['desc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances?offset={{offset}}&limit={{limit}}&orderBy=instanceType&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "instanceType" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get ODS Instances filter by Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Ods Instances: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Ods Instances: Response result includes Ods instance\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results).to.have.lengthOf(1);\r", + "});\r", + "\r", + "pm.test(\"GET Ods Instances: Response result contains requested Ods instance\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results[0].name).to.eql(pm.collectionVariables.get(\"FILTERNAME\"));\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances?offset={{offset}}&limit={{limit}}&name={{FILTERNAME}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "name", + "value": "{{FILTERNAME}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Get ODS Instances filter by InstanceType", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Ods Instances: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Ods Instances: Response result includes Ods Instance\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results).to.have.lengthOf(1);\r", + "});\r", + "\r", + "pm.test(\"GET Ods Instances: Response result contains requested Ods Instance\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results[0].instanceType).to.eql(pm.collectionVariables.get(\"FILTERINSTANCETYPE\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/odsInstances?offset={{offset}}&limit={{limit}}&instanceType={{FILTERINSTANCETYPE}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "instanceType", + "value": "{{FILTERINSTANCETYPE}}" + } + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "/// -------", + "/// Authentication", + "/// -------", + "", + "const jsHelperVar = pm.variables.get('jsHelper');", + "const jsHelper = eval(jsHelperVar);", + "", + "var header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "var guid = pm.variables.replaceIn('{{$guid}}');", + "var client_secret = jsHelper().generateClientSecret();", + "", + " const registerRequest = {", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + " }", + "", + " const tokenRequest = {", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + " }", + "", + " if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + " }", + "", + "function generateAlphanumericString() {", + " const minLength = 8;", + " const maxLength = 24;", + " let result = '';", + " //const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += jsHelper().randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += jsHelper().randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += jsHelper().randomChar('0123456789');", + "", + " for (let i = result.length; i < length; i++) {", + " result += characters.charAt(Math.floor(Math.random() * characters.length));", + " }", + "", + " return jsHelper().shuffleString(result);", + "}", + "", + "/// -------", + "/// ODS Instances", + "/// -------", + "", + "pm.sendRequest(registerRequest,", + " (err, res) => {", + " let error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + " pm.sendRequest(tokenRequest,", + " (err, res) => {", + " let error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "", + " header = {", + " 'Content-Type': 'application/json',", + " 'Authorization': `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`", + " };", + " ", + " if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + " }", + " let totalToCreate = parseInt(pm.collectionVariables.get(\"ODSINSTANCECOUNT\"), 10);", + " var odsInstancesToDelete = [];", + " for (let i = 0; i < totalToCreate; i++) {", + " let name = `TestOdsInstance-${generateAlphanumericString()}`;", + " let instanceType = generateAlphanumericString();", + " ", + " pm.collectionVariables.set(\"FILTERNAME\", name);", + " pm.collectionVariables.set(\"FILTERINSTANCETYPE\", instanceType);", + " ", + " pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'raw',", + " raw: JSON.stringify({ \"name\": `${name}`, \"instanceType\": `${instanceType}`, \"connectionString\": pm.variables.get(\"connectionString\") })", + " }", + " }, (err, res) => {", + " let location = res.headers.get('Location');", + " let matches = location.match(/(\\d+)/);", + " let odsInstanceId = parseInt(matches[0], 10);", + " // odsInstancesToDelete.push({id: odsInstanceId, name: name, instanceType: instanceType});", + " odsInstancesToDelete.push(odsInstanceId);", + " });", + " }", + " pm.collectionVariables.set(\"ODSINSTANCESTODELETE\", odsInstancesToDelete);", + " });", + "});", + "", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "header = {", + " 'Content-Type': 'application/json',", + " 'Accept': '*/*',", + " 'Authorization': `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "function deleteODSInstanceById(id) {", + " pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances/${id}`,", + " method: 'DELETE',", + " header: header", + " }, function (err, res) {", + " if (err) {", + " console.log(\"Error deleting: \" + err);", + " }", + " });", + "}", + "", + "function deleteODSInstances() {", + " ", + " let odsInstancesToDelete = pm.collectionVariables.get(\"ODSINSTANCESTODELETE\").split(\",\");", + "", + " for (let i = 0; i < odsInstancesToDelete.length; i++) {", + " deleteODSInstanceById(odsInstancesToDelete[i]);", + " }", + " ", + "}", + "", + "deleteODSInstances();" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "", + "type": "string" + }, + { + "key": "FILTERNAME", + "value": "", + "type": "string" + }, + { + "key": "FILTERINSTANCETYPE", + "value": "", + "type": "string" + }, + { + "key": "ODSINSTANCECOUNT", + "value": "5", + "type": "string" + }, + { + "key": "ODSINSTANCESTODELETE", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - Profile.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - Profile.postman_collection.json new file mode 100644 index 000000000..5ca6347cd --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - Profile.postman_collection.json @@ -0,0 +1,790 @@ +{ + "info": { + "_postman_id": "25fa645a-e896-41fb-842b-5a7865a60323", + "name": "Admin API E2E 2.0 - Sorting and Filtering - Profile", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "37902114" + }, + "item": [ + { + "name": "Get Profiles order by Default Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Profiles: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result includes profiles\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result is ordered by Name asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [profile => profile.name],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/profiles?offset={{offset}}&limit={{limit}}&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Profiles order by Default Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by Name desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [profile => profile.name],['desc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/profiles?offset={{offset}}&limit={{limit}}&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Profiles order by Id Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Profiles: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result includes profiles\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result is ordered by Id asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [profile => profile.id],['asc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/profiles?offset={{offset}}&limit={{limit}}&orderBy=Id&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "Id" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Profiles order by Id Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by Name desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [profile => profile.id],['desc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/profiles?offset={{offset}}&limit={{limit}}&orderBy=Id&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "Id" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Profiles order by Name Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Profiles: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result includes profiles\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result is ordered by Name asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [profile => profile.name],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/profiles?offset={{offset}}&limit={{limit}}&orderBy=name&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "name" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Profiles order by Name Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Profiles: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result includes profiles\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result is ordered by Name desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [profile => profile.name],['desc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/profiles?offset={{offset}}&limit={{limit}}&orderBy=Name&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "Name" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Profiles filter by Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profiles: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result includes profile\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results).to.have.lengthOf(1);\r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result contains requested profile\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results[0].name).to.eql(pm.collectionVariables.get(\"FILTERNAME\"));\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/profiles?offset={{offset}}&limit={{limit}}&name={{FILTERNAME}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "name", + "value": "{{FILTERNAME}}" + } + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "/// -------\r", + "/// Authentication\r", + "/// -------\r", + "\r", + "const jsHelperVar = pm.variables.get('jsHelper');\r", + "const jsHelper = eval(jsHelperVar);\r", + "var guid = pm.variables.replaceIn('{{$guid}}');\r", + "var client_secret = jsHelper().generateClientSecret();\r", + "var header = {\r", + " 'Content-Type': 'application/x-www-form-urlencoded'\r", + "};\r", + "\r", + "const registerRequest = {\r", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'urlencoded',\r", + " urlencoded: [\r", + " {key: 'ClientId', value: guid },\r", + " {key: 'ClientSecret', value: client_secret },\r", + " {key: 'DisplayName', value: guid }\r", + " ]\r", + " }\r", + "}\r", + "\r", + "const tokenRequest = {\r", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'urlencoded',\r", + " urlencoded: [\r", + " {key: 'client_id', value: guid },\r", + " {key: 'client_secret', value: client_secret },\r", + " {key: 'grant_type', value: \"client_credentials\"},\r", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}\r", + " ]\r", + " }\r", + "}\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + "}\r", + "\r", + "/// -------\r", + "/// Profiles\r", + "/// -------\r", + "\r", + "function generateAlphanumericString() {\r", + " const minLength = 8;\r", + " const maxLength = 24;\r", + " let result = '';\r", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\r", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;\r", + "\r", + " result += jsHelper().randomChar('abcdefghijklmnopqrstuvwxyz');\r", + " result += jsHelper().randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');\r", + " result += jsHelper().randomChar('0123456789');\r", + "\r", + " for (let i = result.length; i < length; i++) {\r", + " result += characters.charAt(Math.floor(Math.random() * characters.length));\r", + " }\r", + "\r", + " return jsHelper().shuffleString(result);\r", + "}\r", + "\r", + "pm.sendRequest(registerRequest,\r", + " (err, res) => {\r", + " let error = res.json().error\r", + " if(error) {\r", + " throw res.json().error_description\r", + " }\r", + "\r", + " pm.sendRequest(tokenRequest,\r", + " (err, res) => {\r", + " let error = res.json().error\r", + " if(error) {\r", + " throw res.json().error_description\r", + " }\r", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)\r", + "\r", + " header = {\r", + " 'Content-Type': 'application/json',\r", + " 'Authorization': `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " };\r", + " \r", + " if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + " }\r", + " let totalToCreate = parseInt(pm.collectionVariables.get(\"PROFILECOUNT\"), 10);\r", + " var profilesToDelete = [];\r", + " for (let i = 0; i < totalToCreate; i++) {\r", + " let name = generateAlphanumericString();\r", + " pm.collectionVariables.set(\"FILTERNAME\", name);\r", + "\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/profiles`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({ \"name\": `${name}`, \"definition\": `` })\r", + " }\r", + " }, (err, res) => {\r", + " let location = res.headers.get('Location');\r", + " let matches = location.match(/(\\d+)/);\r", + " profileId = parseInt(matches[0], 10);\r", + " profilesToDelete.push(profileId)\r", + " });\r", + " }\r", + " pm.collectionVariables.set(\"PROFILESTODELETE\", profilesToDelete);\r", + " });\r", + "});\r", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "header = {\r", + " 'Content-Type': 'application/json',\r", + " 'Accept': '*/*',\r", + " 'Authorization': `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + "}\r", + "let idsToDelete = pm.collectionVariables.get(\"PROFILESTODELETE\").split(\",\");\r", + "for (let i = 0; i < idsToDelete.length; i++) {\r", + " let id = idsToDelete[i];\r", + " pm.sendRequest({ \r", + " url: `${pm.variables.get(\"API_URL\")}/v2/profiles/${id}`,\r", + " method: 'DELETE',\r", + " header: header\r", + " }, function (err, res) {\r", + " if (err) {\r", + " console.log(err);\r", + " }\r", + " });\r", + "}" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "", + "type": "string" + }, + { + "key": "FILTERNAME", + "value": "0" + }, + { + "key": "PROFILECOUNT", + "value": "4", + "type": "string" + }, + { + "key": "PROFILESTODELETE", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - Vendor.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - Vendor.postman_collection.json new file mode 100644 index 000000000..9467944d3 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Sorting and Filtering - Vendor.postman_collection.json @@ -0,0 +1,1565 @@ +{ + "info": { + "_postman_id": "97521082-4124-44ad-ab08-aaf5c30ebd73", + "name": "Admin API E2E 2.0 - Sorting and Filtering - Vendor", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "37902114" + }, + "item": [ + { + "name": "Get Vendors order by Default Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by Company asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [vendor => vendor.company],['asc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors order by Default Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by Company desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [vendor => vendor.company],['desc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors order by Id Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by Id asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [vendor => vendor.id],['asc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&orderBy=id&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "id" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors order by Id Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by Id desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [vendor => vendor.id],['desc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&orderBy=id&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "id" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors order by Company Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by Company asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [vendor => vendor.company],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&orderBy=company&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "company" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors order by Company Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by Company desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [vendor => vendor.company],['desc']);\r", + " \r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&orderBy=Company&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "Company" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors order by ContactEmailAddress Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by contactEmailAddress asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [vendor => vendor.contactEmailAddress],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&orderBy=ContactEmailAddress&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "ContactEmailAddress" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors order by ContactEmailAddress Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by contactEmailAddress desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [vendor => vendor.contactEmailAddress],['desc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&orderBy=ContactEmailAddress&direction=DESC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "ContactEmailAddress" + }, + { + "key": "direction", + "value": "DESC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors order by ContactName Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by ContactName asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [vendor => vendor.contactName],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&orderBy=contactName&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "contactName" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors order by ContactName Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by ContactName desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [vendor => vendor.contactName],['desc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&orderBy=contactName&direction=DESC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "contactName" + }, + { + "key": "direction", + "value": "DESC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors order by NamespacePrefix Asc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by NamespacePrefixes asc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [vendor => vendor.namespacePrefixes],['asc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&orderBy=namespacePrefixes&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "namespacePrefixes" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors order by NamespacePrefix Desc", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var _ = require('lodash');\r", + "\r", + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result is ordered by NamespacePrefixes desc\", function () {\r", + " const results = pm.response.json();\r", + " var expectedSortedOrder = _.orderBy(results, [vendor => vendor.namespacePrefixes],['desc']);\r", + " pm.expect(results).to.eql(expectedSortedOrder);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&orderBy=namespacePrefixes&direction=desc", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "namespacePrefixes" + }, + { + "key": "direction", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors filter by Company", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendor\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results).to.have.lengthOf(1);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result contains requested vendor\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results[0].company).to.eql(pm.collectionVariables.get(\"FILTERCOMPANY\"));\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&company={{FILTERCOMPANY}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "company", + "value": "{{FILTERCOMPANY}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors filter by NamespacePrefix", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendor\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results).to.have.lengthOf(1);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result contains requested vendor\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results[0].namespacePrefixes).to.eql(pm.collectionVariables.get(\"FILTERNAMESPACEPREFIXES\"));\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&namespacePrefixes={{FILTERNAMESPACEPREFIXES}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "namespacePrefixes", + "value": "{{FILTERNAMESPACEPREFIXES}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors filter by ContactName", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendor\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results).to.have.lengthOf(1);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result contains requested vendor\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results[0].contactName).to.eql(pm.collectionVariables.get(\"FILTERCONTACTNAME\"));\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&contactName={{FILTERCONTACTNAME}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "contactName", + "value": "{{FILTERCONTACTNAME}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Vendors filter by ContactEmailAddress", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendor\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results).to.have.lengthOf(1);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors: Response result contains requested vendor\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results[0].contactEmailAddress).to.eql(pm.collectionVariables.get(\"FILTERCONTACTEMAILADDRESS\"));\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&contactEmailAddress={{FILTERCONTACTEMAILADDRESS}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "contactEmailAddress", + "value": "{{FILTERCONTACTEMAILADDRESS}}" + } + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "/// -------\r", + "/// Authentication\r", + "/// -------\r", + "\r", + "const jsHelperVar = pm.variables.get('jsHelper');\r", + "const jsHelper = eval(jsHelperVar);\r", + "var guid = pm.variables.replaceIn('{{$guid}}');\r", + "var client_secret = jsHelper().generateClientSecret();\r", + "var header = {\r", + " 'Content-Type': 'application/x-www-form-urlencoded'\r", + "};\r", + "\r", + "const registerRequest = {\r", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'urlencoded',\r", + " urlencoded: [\r", + " {key: 'ClientId', value: guid },\r", + " {key: 'ClientSecret', value: client_secret },\r", + " {key: 'DisplayName', value: guid }\r", + " ]\r", + " }\r", + "}\r", + "\r", + "const tokenRequest = {\r", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'urlencoded',\r", + " urlencoded: [\r", + " {key: 'client_id', value: guid },\r", + " {key: 'client_secret', value: client_secret },\r", + " {key: 'grant_type', value: \"client_credentials\"},\r", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}\r", + " ]\r", + " }\r", + "}\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + "}\r", + "\r", + "/// -------\r", + "/// Vendors\r", + "/// -------\r", + "\r", + "function generateAlphanumericString() {\r", + " const minLength = 8;\r", + " const maxLength = 24;\r", + " let result = '';\r", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\r", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;\r", + "\r", + " result += jsHelper().randomChar('abcdefghijklmnopqrstuvwxyz');\r", + " result += jsHelper().randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');\r", + " result += jsHelper().randomChar('0123456789');\r", + "\r", + " for (let i = result.length; i < length; i++) {\r", + " result += characters.charAt(Math.floor(Math.random() * characters.length));\r", + " }\r", + "\r", + " return jsHelper().shuffleString(result);\r", + "}\r", + "\r", + "pm.sendRequest(registerRequest,\r", + " (err, res) => {\r", + " let error = res.json().error\r", + " if(error) {\r", + " throw res.json().error_description\r", + " }\r", + "\r", + " pm.sendRequest(tokenRequest,\r", + " (err, res) => {\r", + " let error = res.json().error\r", + " if(error) {\r", + " throw res.json().error_description\r", + " }\r", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)\r", + "\r", + " header = {\r", + " 'Content-Type': 'application/json',\r", + " 'Authorization': `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " };\r", + " \r", + " if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + " }\r", + " let totalToCreate = parseInt(pm.collectionVariables.get(\"VENDORSCOUNT\"), 10);\r", + " var vendorsToDelete = [];\r", + " for (let i = 0; i < totalToCreate; i++) {\r", + " let company = generateAlphanumericString();\r", + " let namespacePrefixes = `uri://${generateAlphanumericString()}.org`;\r", + " let contactName = generateAlphanumericString();\r", + " let contactEmailAddress = `${generateAlphanumericString()}@test-ed-fi.org`;\r", + "\r", + " pm.collectionVariables.set(\"FILTERCOMPANY\", company);\r", + " pm.collectionVariables.set(\"FILTERNAMESPACEPREFIXES\", namespacePrefixes);\r", + " pm.collectionVariables.set(\"FILTERCONTACTNAME\", contactName);\r", + " pm.collectionVariables.set(\"FILTERCONTACTEMAILADDRESS\", contactEmailAddress);\r", + " \r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,\r", + " method: 'POST',\r", + " header: header,\r", + " body: {\r", + " mode: 'raw',\r", + " raw: JSON.stringify({ \"company\": `${company}`, \"namespacePrefixes\": `${namespacePrefixes}`, \"contactName\": `${contactName}`, \"contactEmailAddress\": `${contactEmailAddress}` })\r", + " }\r", + " }, (err, res) => {\r", + " let location = res.headers.get('Location');\r", + " let matches = location.match(/(\\d+)/);\r", + " vendorId = parseInt(matches[0], 10);\r", + " // odsInstancesToDelete.push({id: odsInstanceId, name: name, instanceType: instanceType});\r", + " vendorsToDelete.push(vendorId);\r", + " });\r", + " }\r", + " pm.collectionVariables.set(\"VENDORSTODELETE\", vendorsToDelete);\r", + " });\r", + "});\r", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "header = {\r", + " 'Content-Type': 'application/json',\r", + " 'Accept': '*/*',\r", + " 'Authorization': `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });\r", + "}\r", + "let vendorsToDelete = pm.collectionVariables.get(\"VENDORSTODELETE\").split(\",\");\r", + "for (let i = 0; i < vendorsToDelete.length; i++) {\r", + " let id = vendorsToDelete[i];\r", + " pm.sendRequest({ \r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors/${id}`,\r", + " method: 'DELETE',\r", + " header: header\r", + " }, function (err, res) {\r", + " if (err) {\r", + " console.log(err);\r", + " }\r", + " });\r", + "}" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "", + "type": "string" + }, + { + "key": "FILTERCOMPANY", + "value": "" + }, + { + "key": "FILTERCONTACTNAME", + "value": "" + }, + { + "key": "FILTERNAMESPACEPROFIXES", + "value": "" + }, + { + "key": "FILTERNAMESPACEPREFIXES", + "value": "" + }, + { + "key": "VENDORSCOUNT", + "value": "4", + "type": "string" + }, + { + "key": "VENDORSTODELETE", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Tenants.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Tenants.postman_collection.json new file mode 100644 index 000000000..7f4110607 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Tenants.postman_collection.json @@ -0,0 +1,506 @@ +{ + "info": { + "_postman_id": "170362e2-1d87-42e5-9732-d6fbe264d0ba", + "name": "Admin API E2E 2.0 - Tenants", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "29458798" + }, + "item": [ + { + "name": "V2", + "item": [ + { + "name": "Tenants", + "item": [ + { + "name": "Tenants", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Tenants: Status code is Found\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const GetTenantsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"tenantName\": {", + " \"type\": \"string\"", + " },", + " \"adminConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " },", + " },", + " \"securityConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " }", + " }", + " },", + " \"required\": [", + " \"tenantName\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET Tenants: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetTenantsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants" + ] + } + }, + "response": [] + }, + { + "name": "Tenants - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + "});", + "", + "pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "tenants" + ] + } + }, + "response": [] + }, + { + "name": "Tenant without 's'", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Tenants: Status code is Invalid\", function () {", + " pm.response.to.have.status(404);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenant", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenant" + ] + } + }, + "response": [] + }, + { + "name": "Tenants by Tenant Name - Multitenant", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " pm.test(\"GET Tenants by Tenant Id: Status code is Found\", function () {", + " pm.response.to.have.status(200);", + " });", + "", + " const response = pm.response.json();", + " const result = pm.response.json();", + "", + " pm.test(\"GET Tenant Name: Response result matches tenant\", function () {", + " pm.expect(result.tenantName).to.equal(\"tenant1\");", + " pm.expect(result.adminConnectionString.host).to.not.equal(null);", + " pm.expect(result.adminConnectionString.database).to.not.equal(null);", + " pm.expect(result.securityConnectionString.host).to.not.equal(null);", + " pm.expect(result.securityConnectionString.database).to.not.equal(null);", + " });", + "", + " const GetTenantsByTenantSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"tenantName\": {", + " \"type\": \"string\"", + " },", + " \"adminConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " },", + " },", + " \"securityConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " }", + " }", + " },", + " \"required\": [", + " \"tenantName\"", + " ]", + " }", + "", + " pm.test(\"GET Tenants by Tenant Id: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetTenantsByTenantSchema);", + " });", + "}", + "else {", + " pm.test(\"GET Tenants by Tenant Id: Status code is Found\", function () {", + " pm.response.to.have.status(404);", + " });", + "}" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenants/tenant1", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants", + "tenant1" + ] + } + }, + "response": [] + }, + { + "name": "Tenants by Tenant Name - Singletenant", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"false\") {", + " pm.test(\"GET Tenants by Tenant Id: Status code is Found\", function () {", + " pm.response.to.have.status(200);", + " });", + "", + " const response = pm.response.json();", + " const result = pm.response.json();", + "", + " pm.test(\"GET Tenant Name: Response result matches tenant\", function () {", + " pm.expect(result.tenantName).to.equal(\"default\");", + " pm.expect(result.adminConnectionString.host).to.not.equal(null);", + " pm.expect(result.adminConnectionString.database).to.not.equal(null);", + " pm.expect(result.securityConnectionString.host).to.not.equal(null);", + " pm.expect(result.securityConnectionString.database).to.not.equal(null);", + " });", + "", + " const GetTenantsByTenantSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"tenantName\": {", + " \"type\": \"string\"", + " },", + " \"adminConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " },", + " },", + " \"securityConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " }", + " }", + " },", + " \"required\": [", + " \"tenantName\"", + " ]", + " }", + "", + " pm.test(\"GET Tenants by Tenant Id: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetTenantsByTenantSchema);", + " });", + "}" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenants/default", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants", + "default" + ] + } + }, + "response": [] + }, + { + "name": "Tenants by Tenant Name Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Tenants: Status code is Found\", function () {", + " pm.response.to.have.status(404);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenants/notexistingtenantname", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants", + "notexistingtenantname" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - User Management.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - User Management.postman_collection.json new file mode 100644 index 000000000..edbd58304 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - User Management.postman_collection.json @@ -0,0 +1,949 @@ +{ + "info": { + "_postman_id": "28ac83c0-a342-484e-9941-922fc7205e3d", + "name": "Admin API E2E 2.0 - User Management", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "42536960", + "_collection_link": "https://test33-6887.postman.co/workspace/AdminApi1182~87697d41-7b5a-4e20-baad-4161b31b430e/collection/42536960-28ac83c0-a342-484e-9941-922fc7205e3d?action=share&source=collection_link&creator=42536960" + }, + "item": [ + { + "name": "Landing", + "item": [ + { + "name": "Landing Page", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Landing: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"GET Landing: Response includes expected properties\", function () {\r", + " pm.expect(response).to.have.property(\"version\");\r", + " pm.expect(response).to.have.property(\"build\");\r", + "});\r", + "\r", + "const GetSchemaLanding = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"version\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"build\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"version\",\r", + " \"build\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Landing: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetSchemaLanding);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}", + "host": [ + "{{API_URL}}" + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "noauth" + } + }, + { + "name": "User Management", + "item": [ + { + "name": "Register", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Register: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json().result;\r", + "\r", + "pm.test(\"POST Register: Response matches success format\", function () {\r", + " pm.expect(response.status).to.equal(200);\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"POST Register: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"client\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"registered\");\r", + "});\r", + "\r", + "const PostRegisterSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"status\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\",\r", + " \"status\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Register: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostRegisterSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "function generateClientSecret() {\r", + " const minLength = 32;\r", + " const maxLength = 128;\r", + " let result = '';\r", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\r", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';\r", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;\r", + "\r", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');\r", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');\r", + " result += randomChar('0123456789');\r", + " result += randomChar(specialCharacters);\r", + "\r", + " for (let i = result.length; i < length; i++) {\r", + " const charactersPlusSpecial = characters + specialCharacters;\r", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));\r", + " }\r", + "\r", + " return shuffleString(result);\r", + "}\r", + "\r", + "function randomChar(str) {\r", + " return str.charAt(Math.floor(Math.random() * str.length));\r", + "}\r", + "\r", + "function shuffleString(str) {\r", + " const array = str.split('');\r", + " for (let i = array.length - 1; i > 0; i--) {\r", + " const j = Math.floor(Math.random() * (i + 1));\r", + " [array[i], array[j]] = [array[j], array[i]];\r", + " }\r", + " return array.join('');\r", + "}\r", + "\r", + "pm.collectionVariables.set(\"RegisteredClientId\", pm.variables.replaceIn('{{$guid}}'));\r", + "pm.collectionVariables.set(\"RegisteredClientSecret\", generateClientSecret());\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ClientId", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "ClientSecret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "DisplayName", + "value": "Postman Test", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/register", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Register - Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Register Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Register Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"ClientId\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ClientSecret\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"DisplayName\"].length).to.equal(1);\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ClientId", + "value": "", + "type": "text" + }, + { + "key": "ClientSecret", + "value": "", + "type": "text" + }, + { + "key": "DisplayName", + "value": "", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/register", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Register - Already Exists", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Register Already Exists: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Register Already Exists: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Already Exists: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Already Exists: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ClientId.length).to.equal(1);\r", + " pm.expect(response.errors.ClientId[0]).to.contain(\"already exists\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ClientId", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "ClientSecret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "DisplayName", + "value": "{{UserName}}", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/register", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Register - Invalid ClientSecret", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Register Already Exists: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Register Already Exists: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Already Exists: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Invalid ClientSecret: Response errors include message\", function () {\r", + " pm.expect(response.errors.ClientSecret.length).to.equal(1);\r", + " pm.expect(response.errors.ClientSecret[0]).to.contain(\"must contain at least one lowercase letter, one uppercase letter, one number, and one special character, and must be 32 to 128 characters\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"InvalidClientSecret\", 'invalidvalue');" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ClientId", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "ClientSecret", + "value": "{{InvalidClientSecret}}", + "type": "text" + }, + { + "key": "DisplayName", + "value": "Test", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/register", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token: Response includes token\", function () {\r", + " pm.expect(response).to.have.property(\"access_token\");\r", + " pm.expect(response).to.have.property(\"token_type\");\r", + " pm.expect(response).to.have.property(\"expires_in\");\r", + "\r", + " pm.expect(response[\"token_type\"]).to.equal(\"Bearer\");\r", + "});\r", + "\r", + "const PostTokenSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"access_token\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"token_type\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"expires_in\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"access_token\",\r", + " \"token_type\",\r", + " \"expires_in\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Token: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostTokenSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/full_access", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Invalid: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{$guid}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Invalid Grant Type", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Invalid Grant Type: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(401);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Invalid Grant Type: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + " pm.expect(response[\"error_description\"]).to.contain(\"Access Denied. Please review your information and try again.\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "authorization_code", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/full_access", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Invalid Scope", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Invalid Scope: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Invalid Scope: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\"); \r", + " pm.expect(response).to.have.property(\"error\", \"invalid_scope\");\r", + " pm.expect(response).to.have.property(\"error_description\", \"The request is missing required scope claims or has invalid scope values\");\r", + "\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{$guid}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "NOT_REAL/SCOPE", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Incorrect Secret", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Incorrect Secret: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(401);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Incorrect Secret: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + " pm.expect(response[\"error_description\"]).to.contain(\"Access Denied. Please review your information and try again.\");\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"RegisteredClientId\");\r", + "pm.collectionVariables.unset(\"RegisteredClientSecret\");" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{$guid}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/full_access", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "noauth" + } + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "InvalidClientSecret", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Validate Exception Content Type.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Validate Exception Content Type.postman_collection.json new file mode 100644 index 000000000..2cb3c4462 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Validate Exception Content Type.postman_collection.json @@ -0,0 +1,111 @@ +{ + "info": { + "_postman_id": "ae24c224-fccb-4ec4-9ecb-3b5e99f340c8", + "name": "Admin API E2E 2.0 - Validate Exception Content Type", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", + "_exporter_id": "40101658" + }, + "item": [ + { + "name": "Vendors - Invalid Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response with content type error\", function () {\r", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/problem+json\");\r", + "});\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response with content type error\", function () {\r", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.not.include(\"application/json\");\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"\",\r\n \"namespacePrefixes\": \"\",\r\n \"contactName\": \"\",\r\n \"contactEmailAddress\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}&orderBy=id&direction=ASC", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + }, + { + "key": "orderBy", + "value": "id" + }, + { + "key": "direction", + "value": "ASC" + } + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "bearer", + "bearer": { + "token": "{{TOKEN}}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Vendors.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Vendors.postman_collection.json new file mode 100644 index 000000000..8ba0a3d63 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Vendors.postman_collection.json @@ -0,0 +1,1661 @@ +{ + "info": { + "_postman_id": "814b8a32-96d3-4a65-b400-fe962037bdd0", + "name": "Admin API E2E 2.0 - Vendors", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "Vendors", + "item": [ + { + "name": "Vendors - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Test Company\",\r\n \"namespacePrefixes\": \"uri://ed-fi.org\",\r\n \"contactName\": \"Test User\",\r\n \"contactEmailAddress\": \"test@test-ed-fi.org\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors" + ] + } + }, + "response": [] + }, + { + "name": "Vendors", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Vendors: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test(\"POST Vendors: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " const id = pm.response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"CreatedVendorId\", id);\r", + " }\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Test Company\",\r\n \"namespacePrefixes\": \"uri://ed-fi.org\",\r\n \"contactName\": \"Test User\",\r\n \"contactEmailAddress\": \"test@test-ed-fi.org\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ] + } + }, + "response": [ + { + "name": "Vendor with multiple namespaces", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": {{CompanyName}},\r\n \"namespacePrefixes\": \"uri://ed-fi.org,uri://academicbenchmarks.com\",\r\n \"contactName\": {{ContactName}},\r\n \"contactEmailAddress\": {{ContactEmail}}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "vendors" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Date", + "value": "Thu, 02 Jun 2022 23:13:53 GMT" + }, + { + "key": "Server", + "value": "Kestrel" + }, + { + "key": "Location", + "value": "/Vendors/2" + }, + { + "key": "Transfer-Encoding", + "value": "chunked" + } + ], + "cookie": [], + "body": "{\n \"result\": {\n \"vendorId\": 2,\n \"company\": \"Test Company\",\n \"namespacePrefixes\": \"uri://ed-fi.org,uri://academicbenchmarks.com\",\n \"contactName\": \"Test User\",\n \"contactEmailAddress\": \"test@test-ed-fi.org\"\n },\n \"status\": 201,\n \"title\": \"Vendor created successfully\"\n}" + } + ] + }, + { + "name": "Vendors - Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Vendors Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Company\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ContactName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ContactEmailAddress\"].length).to.equal(2);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"\",\r\n \"namespacePrefixes\": \"\",\r\n \"contactName\": \"\",\r\n \"contactEmailAddress\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Invalid Prefix exceed length", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Vendors Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"NamespacePrefixes\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Test Company\",\r\n \"namespacePrefixes\": \"uri://abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.org\",\r\n \"contactName\": \"Test User\",\r\n \"contactEmailAddress\": \"test@test-ed-fi.org\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ] + } + }, + "response": [] + }, + { + "name": "Vendors", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetVendorsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetVendorsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Without Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetVendorsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetVendorsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Without Limit and Offset", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetVendorsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetVendorsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Without Limit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetVendorsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetVendorsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Vendors by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendor ID: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET Vendor ID: Response result matches vendor\", function () {\r", + " const vendorId = parseInt(pm.collectionVariables.get(\"CreatedVendorId\"));\r", + " \r", + " pm.expect(result.id).to.equal(vendorId);\r", + " pm.expect(result.company).to.equal(\"Test Company\");\r", + " pm.expect(result.namespacePrefixes).to.equal(\"uri://ed-fi.org\");\r", + " pm.expect(result.contactName).to.equal(\"Test User\");\r", + " pm.expect(result.contactEmailAddress).to.equal(\"test@test-ed-fi.org\");\r", + "});\r", + "\r", + "const GetVendorIdSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET VendorsID: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetVendorIdSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors with Application", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors with Application: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET Vendors With Application: Response result includes application\", function () {\r", + " pm.expect(result[0].applicationName).to.equal(\"Other Vendor with Application\");\r", + " pm.expect(result[0].claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result[0].educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result[0].profileIds.length).to.equal(0);\r", + " pm.expect(result[0].odsInstanceIds.length).to.equal(1);\r", + "});\r", + "\r", + "const GetVendorWithApplicationSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"integer\"\r", + " }\r", + " ]\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceIds\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Vendors with Application: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetVendorWithApplicationSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "let header = {", + " \"Content-Type\": \"application/json\",", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'raw',", + " raw: JSON.stringify({", + " \"company\": \"Other Company With Application\",", + " \"namespacePrefixes\": \"uri://ed-fi.org\",", + " \"contactName\": \"Other Company with Application User\",", + " \"contactEmailAddress\": \"otherapplicationwithcompany@example.com\"", + " }),", + " }", + "},", + " function (vendorErr, vendorResponse) {", + " if (vendorErr) { console.log(\"Error in Pre-request:\", vendorErr); }", + " const id = vendorResponse.headers.get(\"Location\").split(\"/\")[2];", + " if (!id) { console.log('Error in Pre-request: vendorID missing from response. Response is:', vendorHeader); }", + " pm.collectionVariables.set(\"OtherApplicationVendorIdwithVendor\", id);", + "", + " pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/v2/odsInstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,", + " method: 'GET',", + " header: header", + " },", + " function (err, response) {", + " if (err) { console.log(\"Error in Pre-request:\", err); }", + " const json = response.json();", + " if (!json[0].id) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }", + " else {", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].id);", + " }", + " pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/v2/applications`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'raw',", + " raw: JSON.stringify({", + " \"applicationName\": \"Other Vendor with Application\",", + " \"vendorId\": pm.collectionVariables.get(\"OtherApplicationVendorIdwithVendor\"),", + " \"claimSetName\": \"Ed-Fi Sandbox\",", + " \"profileIds\": [],", + " \"educationOrganizationIds\": [983438],", + " \"odsInstanceIds\": [pm.collectionVariables.get(\"ODSInstanceId\")]", + " }),", + " }", + " },", + " function (appErr, appResonse) {", + " if (appErr) { console.log(\"Error in Pre-request:\", appErr); }", + " const appJson = appResonse.json();", + " if (!appJson.id) { console.log('Error in Pre-request: applicationId missing from response. Response is:', appJson); }", + " else {", + " pm.collectionVariables.set(\"OtherApplicationId\", appJson.id);", + " }", + " });", + " });", + "", + " });", + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors/{{OtherApplicationVendorIdwithVendor}}/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{OtherApplicationVendorIdwithVendor}}", + "applications" + ] + } + }, + "response": [] + }, + { + "name": "Vendors", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "let header = {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + "};\r", + "\r", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {\r", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;\r", + "}\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors/${pm.collectionVariables.get(\"CreatedVendorId\")}`,\r", + " method: 'GET',\r", + " header: header\r", + "},\r", + "function (profileErr, profileResponse) {\r", + " if(profileErr) \r", + " { \r", + " console.log(\"Error :\", profileErr); \r", + " } \r", + " const result = profileResponse.json();\r", + " pm.test(\"PUT Profiles: Response includes updated profile\", function () {\r", + " pm.expect(result.company).to.equal(\"Updated Test Company\");\r", + " pm.expect(result.namespacePrefixes).to.equal(\"uri://academicbenchmarks.com\");\r", + " pm.expect(result.contactName).to.equal(\"Updated User\");\r", + " pm.expect(result.contactEmailAddress).to.equal(\"updated@example.com\");\r", + " });\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Updated Test Company\",\r\n \"namespacePrefixes\": \"uri://academicbenchmarks.com\",\r\n \"contactName\": \"Updated User\",\r\n \"contactEmailAddress\": \"updated@example.com\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Updated Test Company\",\r\n \"namespacePrefixes\": \"uri://academicbenchmarks.com\",\r\n \"contactName\": \"Updated User\",\r\n \"contactEmailAddress\": \"updated@example.com\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Vendors Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT Vendors Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Vendors Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Vendors Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Company\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ContactName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ContactEmailAddress\"].length).to.equal(2);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"\",\r\n \"contactName\": \"\",\r\n \"contactEmailAddress\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"message\");\r", + "});\r", + "\r", + "pm.test(\"Response title is helpful and accurate\", function () {\r", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DELETE Vendors: Response matches success format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE Vendors: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"vendor\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "\r", + "const DeleteVendorSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"DELETE Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(DeleteVendorSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Vendors NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"vendor\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedVendorId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors with Invalid Application - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Vendors NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"vendor\");\r", + " pm.expect(response.title).to.contain(\"0\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors/0/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "0", + "applications" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Vendors NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT Vendors NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT Vendors NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"vendor\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedVendorId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Updated Test Company\",\r\n \"namespacePrefixes\": \"uri://academicbenchmarks.com\",\r\n \"contactName\": \"Updated User\",\r\n \"contactEmailAddress\": \"updated@example.com\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL Vendors NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"DEL Vendors NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DEL Vendors NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"vendor\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedVendorId\"));\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"CreatedVendorId\");\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "0", + "value": { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + }, + "type": "any" + }, + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "OtherApplicationVendorIdwithVendor", + "value": "" + }, + { + "key": "OtherApplicationId", + "value": "" + }, + { + "key": "ODSInstanceId", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0.postman_collection.json new file mode 100644 index 000000000..b718a4035 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0.postman_collection.json @@ -0,0 +1,8477 @@ +{ + "info": { + "_postman_id": "313c2b62-cb3a-4563-87af-ad58c05ca795", + "name": "Admin API E2E 2.0", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "42536960" + }, + "item": [ + { + "name": "Landing", + "item": [ + { + "name": "Landing Page", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Landing: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"GET Landing: Response includes expected properties\", function () {\r", + " pm.expect(response).to.have.property(\"version\");\r", + " pm.expect(response).to.have.property(\"build\");\r", + "});\r", + "\r", + "const GetSchemaLanding = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"version\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"build\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"version\",\r", + " \"build\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Landing: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetSchemaLanding);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}", + "host": [ + "{{API_URL}}" + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "noauth" + } + }, + { + "name": "User Management", + "item": [ + { + "name": "Register", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Register: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json().result;\r", + "\r", + "pm.test(\"POST Register: Response matches success format\", function () {\r", + " pm.expect(response.status).to.equal(200);\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"POST Register: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"client\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"registered\");\r", + "});\r", + "\r", + "const PostRegisterSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"status\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\",\r", + " \"status\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Register: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostRegisterSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"RegisteredClientId\", pm.variables.replaceIn('{{$guid}}'));\r", + "pm.collectionVariables.set(\"RegisteredClientSecret\", pm.variables.replaceIn('{{$guid}}'));\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ClientId", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "ClientSecret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "DisplayName", + "value": "Postman Test", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/register", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Register - Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Register Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Register Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"ClientId\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ClientSecret\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"DisplayName\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ClientId", + "value": "", + "type": "text" + }, + { + "key": "ClientSecret", + "value": "", + "type": "text" + }, + { + "key": "DisplayName", + "value": "", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/register", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Register - Already Exists", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Register Already Exists: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Register Already Exists: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Already Exists: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Already Exists: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ClientId.length).to.equal(1);\r", + " pm.expect(response.errors.ClientId[0]).to.contain(\"already exists\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ClientId", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "ClientSecret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "DisplayName", + "value": "{{UserName}}", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/register", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token: Response includes token\", function () {\r", + " pm.expect(response).to.have.property(\"access_token\");\r", + " pm.expect(response).to.have.property(\"token_type\");\r", + " pm.expect(response).to.have.property(\"expires_in\");\r", + "\r", + " pm.expect(response[\"token_type\"]).to.equal(\"Bearer\");\r", + "});\r", + "\r", + "const PostTokenSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"access_token\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"token_type\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"expires_in\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"access_token\",\r", + " \"token_type\",\r", + " \"expires_in\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Token: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostTokenSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/full_access", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Invalid: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + " pm.expect(response).to.have.property(\"error_uri\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{$guid}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Invalid Grant Type", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Invalid Grant Type: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Invalid Grant Type: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + " pm.expect(response).to.have.property(\"error_uri\");\r", + "\r", + " pm.expect(response[\"error_description\"]).to.contain(\"grant_type\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "authorization_code", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/full_access", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Invalid Scope", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Invalid Scope: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Invalid Scope: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + " pm.expect(response).to.have.property(\"error_uri\");\r", + "\r", + " pm.expect(response[\"error_description\"]).to.contain(\"scope\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{$guid}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "NOT_REAL/SCOPE", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Incorrect Secret", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Incorrect Secret: Status code is Unauthorized\", function () {\r", + " pm.response.to.have.status(401);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Incorrect Secret: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + " pm.expect(response).to.have.property(\"error_uri\");\r", + "\r", + " pm.expect(response[\"error_description\"]).to.contain(\"credentials\");\r", + " pm.expect(response[\"error_description\"]).to.contain(\"invalid\");\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"RegisteredClientId\");\r", + "pm.collectionVariables.unset(\"RegisteredClientSecret\");" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{$guid}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/full_access", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "noauth" + } + }, + { + "name": "User Management Authorization Scopes", + "item": [ + { + "name": "Register", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Register: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json().result;\r", + "\r", + "pm.test(\"POST Register: Response matches success format\", function () {\r", + " pm.expect(response.status).to.equal(200);\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"POST Register: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"client\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"registered\");\r", + "});\r", + "\r", + "const PostRegisterSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"status\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\",\r", + " \"status\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Register: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostRegisterSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"RegisteredClientId\", pm.variables.replaceIn('{{$guid}}'));\r", + "pm.collectionVariables.set(\"RegisteredClientSecret\", pm.variables.replaceIn('TKN{{$guid}}'));\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ClientId", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "ClientSecret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "DisplayName", + "value": "Postman Test", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/register", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Token - Full - Access", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response != null ? pm.response.json() ? null;\r", + "pm.collectionVariables.set(\"FULL_ACCESS_TOKEN\", response?.access_token);\r", + "\r", + "pm.test(\"POST Token: Response includes token\", function () {\r", + " pm.expect(response).to.have.property(\"access_token\");\r", + " pm.expect(response).to.have.property(\"token_type\");\r", + " pm.expect(response).to.have.property(\"expires_in\");\r", + "\r", + " pm.expect(response[\"token_type\"]).to.equal(\"Bearer\");\r", + "});\r", + "\r", + "const PostTokenSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"access_token\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"token_type\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"expires_in\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"access_token\",\r", + " \"token_type\",\r", + " \"expires_in\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Token: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostTokenSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "packages": {}, + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/full_access", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Full Access", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSets: Response result includes claimsets\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "});\r", + "\r", + "const GetClaimSetsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSet: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{FULL_ACCESS_TOKEN}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Token - Tenant Access", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "pm.collectionVariables.set(\"TENANT_ACCESS_TOKEN\", response.access_token);\r", + "\r", + "pm.test(\"POST Token: Response includes token\", function () {\r", + " pm.expect(response).to.have.property(\"access_token\");\r", + " pm.expect(response).to.have.property(\"token_type\");\r", + " pm.expect(response).to.have.property(\"expires_in\");\r", + "\r", + " pm.expect(response[\"token_type\"]).to.equal(\"Bearer\");\r", + "});\r", + "\r", + "const PostTokenSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"access_token\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"token_type\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"expires_in\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"access_token\",\r", + " \"token_type\",\r", + " \"expires_in\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Token: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostTokenSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/tenant_access", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Tenant Access", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSets: Status code is 403 for Tenant Access\", function () {\r", + " pm.response.to.have.status(403);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TENANT_ACCESS_TOKEN}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Token - Worker", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "pm.collectionVariables.set(\"WORKER_PROCESS_TOKEN\", response.access_token);\r", + "\r", + "pm.test(\"POST Token: Response includes token\", function () {\r", + " pm.expect(response).to.have.property(\"access_token\");\r", + " pm.expect(response).to.have.property(\"token_type\");\r", + " pm.expect(response).to.have.property(\"expires_in\");\r", + "\r", + " pm.expect(response[\"token_type\"]).to.equal(\"Bearer\");\r", + "});\r", + "\r", + "const PostTokenSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"access_token\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"token_type\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"expires_in\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"access_token\",\r", + " \"token_type\",\r", + " \"expires_in\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Token: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostTokenSchema);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/worker", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Worker", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSets: Status code is 403 for Worker Scope\", function () {\r", + " pm.response.to.have.status(403);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{WORKER_PROCESS_TOKEN}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimSets?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimSets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Token - Invalid Scope", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Invalid Scope: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Invalid Scope: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + " pm.expect(response).to.have.property(\"error_uri\");\r", + "\r", + " pm.expect(response[\"error_description\"]).to.contain(\"scope\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{$guid}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "NOT_REAL/SCOPE", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "noauth" + } + }, + { + "name": "v2", + "item": [ + { + "name": "Vendors", + "item": [ + { + "name": "Vendors", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Vendors: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"POST Vendors: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " pm.response.to.be.header(\"Location\", `/vendors/${result.id}`);\r", + "});\r", + "\r", + "pm.test(\"POST Vendors: Response result includes vendor info\", function () {\r", + " pm.expect(result.company).to.equal(\"Test Company\");\r", + " pm.expect(result.namespacePrefixes).to.equal(\"uri://ed-fi.org\");\r", + " pm.expect(result.contactName).to.equal(\"Test User\");\r", + " pm.expect(result.contactEmailAddress).to.equal(\"test@test-ed-fi.org\");\r", + "});\r", + "\r", + "if(result.id) {\r", + " pm.collectionVariables.set(\"CreatedVendorId\", result.id);\r", + "}\r", + "\r", + "const PostVendorSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostVendorSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Test Company\",\r\n \"namespacePrefixes\": \"uri://ed-fi.org\",\r\n \"contactName\": \"Test User\",\r\n \"contactEmailAddress\": \"test@test-ed-fi.org\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ] + } + }, + "response": [ + { + "name": "Vendor with multiple namespaces", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": {{CompanyName}},\r\n \"namespacePrefixes\": \"uri://ed-fi.org,uri://academicbenchmarks.com\",\r\n \"contactName\": {{ContactName}},\r\n \"contactEmailAddress\": {{ContactEmail}}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "vendors" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Date", + "value": "Thu, 02 Jun 2022 23:13:53 GMT" + }, + { + "key": "Server", + "value": "Kestrel" + }, + { + "key": "Location", + "value": "/Vendors/2" + }, + { + "key": "Transfer-Encoding", + "value": "chunked" + } + ], + "cookie": [], + "body": "{\n \"result\": {\n \"vendorId\": 2,\n \"company\": \"Test Company\",\n \"namespacePrefixes\": \"uri://ed-fi.org,uri://academicbenchmarks.com\",\n \"contactName\": \"Test User\",\n \"contactEmailAddress\": \"test@test-ed-fi.org\"\n },\n \"status\": 201,\n \"title\": \"Vendor created successfully\"\n}" + } + ] + }, + { + "name": "Vendors - Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Vendors Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Company\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ContactName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ContactEmailAddress\"].length).to.equal(2);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"\",\r\n \"namespacePrefixes\": \"\",\r\n \"contactName\": \"\",\r\n \"contactEmailAddress\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ] + } + }, + "response": [] + }, + { + "name": "Vendors", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfVendor = results.map(\r", + " function(vendor) { return vendor.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedVendorId\"));\r", + "\r", + " const result = results[indexOfVendor];\r", + " pm.expect(result.company).to.equal(\"Test Company\");\r", + " pm.expect(result.namespacePrefixes).to.equal(\"uri://ed-fi.org\");\r", + " pm.expect(result.contactName).to.equal(\"Test User\");\r", + " pm.expect(result.contactEmailAddress).to.equal(\"test@test-ed-fi.org\");\r", + "});\r", + "\r", + "const GetVendorsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetVendorsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Vendors by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendor ID: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET Vendor ID: Response result matches vendor\", function () {\r", + " const vendorId = pm.collectionVariables.get(\"CreatedVendorId\");\r", + " \r", + " pm.expect(result.id).to.equal(vendorId);\r", + " pm.expect(result.company).to.equal(\"Test Company\");\r", + " pm.expect(result.namespacePrefixes).to.equal(\"uri://ed-fi.org\");\r", + " pm.expect(result.contactName).to.equal(\"Test User\");\r", + " pm.expect(result.contactEmailAddress).to.equal(\"test@test-ed-fi.org\");\r", + "});\r", + "\r", + "const GetVendorIdSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET VendorsID: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetVendorIdSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"PUT Vendors: Response result includes updated vendor\", function () {\r", + " pm.expect(result.company).to.equal(\"Updated Test Company\");\r", + " pm.expect(result.namespacePrefixes).to.equal(\"uri://academicbenchmarks.com\");\r", + " pm.expect(result.contactName).to.equal(\"Updated User\");\r", + " pm.expect(result.contactEmailAddress).to.equal(\"updated@example.com\");\r", + "});\r", + "\r", + "const PutVendorsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"PUT Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PutVendorsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Updated Test Company\",\r\n \"namespacePrefixes\": \"uri://academicbenchmarks.com\",\r\n \"contactName\": \"Updated User\",\r\n \"contactEmailAddress\": \"updated@example.com\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Vendors Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT Vendors Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Vendors Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Vendors Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Company\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ContactName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ContactEmailAddress\"].length).to.equal(2);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"\",\r\n \"contactName\": \"\",\r\n \"contactEmailAddress\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DELETE Vendors: Response matches success format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE Vendors: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"vendor\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "\r", + "const DeleteVendorSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"DELETE Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(DeleteVendorSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Vendors NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"vendor\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedVendorId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Vendors NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT Vendors NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT Vendors NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"vendor\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedVendorId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Updated Test Company\",\r\n \"namespacePrefixes\": \"uri://academicbenchmarks.com\",\r\n \"contactName\": \"Updated User\",\r\n \"contactEmailAddress\": \"updated@example.com\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL Vendors NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"DEL Vendors NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DEL Vendors NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"vendor\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedVendorId\"));\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"CreatedVendorId\");\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Application", + "item": [ + { + "name": "Applications", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Application Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Application User\",\r", + " \"contactEmailAddress\": \"application@example.com\"\r", + " }), \r", + " }\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json.id) { console.log('Error in Pre-request: vendorID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ApplicationVendorId\", json.id);\r", + " }\r", + "});\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v1/odsinstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " }\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json[0].odsInstanceId) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].odsInstanceId);\r", + " }\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "\r", + "pm.test(\"POST Applications: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " pm.response.to.be.header(\"Location\", `/applications/${result.id}`);\r", + "});\r", + "\r", + "pm.test(\"POST Applications: Response result includes application key and secret\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + " pm.expect(result).to.have.property(\"key\");\r", + " pm.expect(result).to.have.property(\"secret\");\r", + "});\r", + "\r", + "if(result.id) {\r", + " pm.collectionVariables.set(\"CreatedApplicationId\", result.id);\r", + "}\r", + "\r", + "const PostApplicationSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"key\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"secret\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"key\",\r", + " \"secret\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Applications: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostApplicationSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceId\": {{ODSInstanceId}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Applications Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"ApplicationName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ClaimSetName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"EducationOrganizationIds\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"OdsInstanceId\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [],\r\n \"odsInstanceId\": 0\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Vendor", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications Invalid Vendor: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Applications Invalid Vendor: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid Vendor: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid Vendor: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.VendorId.length).to.equal(1);\r", + " pm.expect(response.errors.VendorId[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": 9999,\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceId\": 10\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Profile", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications Invalid Profile: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"POST Applications Invalid Profile: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid Profile: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid Profile: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ProfileIds.length).to.equal(1);\r", + " pm.expect(response.errors.ProfileIds[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [ 9999 ],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceId\": {{ODSInstanceId}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid OdsInstance", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications Invalid ODSInstance: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"POST Applications Invalid ODSInstance: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid ODSInstance: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid ODSInstance: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.OdsInstanceId.length).to.equal(1);\r", + " pm.expect(response.errors.OdsInstanceId[0]).to.contain(\"provide valid ods instance id.\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [ 9999 ],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceId\": 0\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, + { + "name": "Applications", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfApplication = results.map(\r", + " function(application) { return application.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "\r", + " const result = results[indexOfApplication];\r", + " pm.expect(result.applicationName).to.equal(\"Test Application\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceid).to.not.equal(0);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response results do not include key or secret\", function () {\r", + " results.forEach(function(result, i) {\r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + " });\r", + "});\r", + "\r", + "const GetApplicationsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceId\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Applications: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApplicationsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Applications by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ApplicationID: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET ApplicationID: Response result matches application\", function () {\r", + " const applicationId = pm.collectionVariables.get(\"CreatedApplicationId\");\r", + " \r", + " pm.expect(result.id).to.equal(applicationId);\r", + " pm.expect(result.applicationName).to.equal(\"Test Application\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceId).to.not.equal(0);\r", + "});\r", + "\r", + "pm.test(\"GET ApplicationID: Response result does not include key or secret\", function () { \r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + "});\r", + "\r", + "const GetApplicationId = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceId\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ApplicationId: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApplicationId);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications by Vendor", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Application by Vendor: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Application by Vendor: Response result includes applications\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfApplication = results.map(\r", + " function(application) { return application.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "\r", + " const result = results[indexOfApplication];\r", + " pm.expect(result.applicationName).to.equal(\"Test Application\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceId).to.not.equal(0);\r", + "});\r", + "\r", + "pm.test(\"GET Application by Vendor: Response result is filtered by vendor\", function () {\r", + " const resultApplicationIds = results.map(\r", + " function(application) { return application.id; }\r", + " );\r", + "\r", + " pm.expect(resultApplicationIds).to.contain(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + " pm.expect(resultApplicationIds).to.not.contain(pm.collectionVariables.get(\"OtherApplicationId\"));\r", + "});\r", + "\r", + "pm.test(\"GET Application by Vendor: Response results do not include key or secret\", function () {\r", + " results.forEach(function(result, i) {\r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + " });\r", + "});\r", + "\r", + "const GetApplicationByVendor = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceId\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Application by Vendor: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApplicationByVendor);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Other Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Other Application User\",\r", + " \"contactEmailAddress\": \"otherapplication@example.com\"\r", + " }), \r", + " }\r", + "},\r", + "function (vendorErr, vendorResponse) {\r", + " if(vendorErr) { console.log(\"Error in Pre-request:\", vendorErr); }\r", + " const vendorJson = vendorResponse.json();\r", + " if(!vendorJson.id) { console.log('Error in Pre-request: vendorID missing from response. Response is:', vendorJson); }\r", + " pm.collectionVariables.set(\"OtherApplicationVendorId\", vendorJson.id);\r", + "\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/applications`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"applicationName\": \"Other Vendor Application\",\r", + " \"vendorId\": pm.collectionVariables.get(\"OtherApplicationVendorId\"),\r", + " \"claimSetName\": \"Ed-Fi Sandbox\",\r", + " \"profileIds\": [],\r", + " \"educationOrganizationIds\": [ 255901 ],\r", + " \"odsInstanceId\": 1\r", + " }),\r", + " }\r", + " }, \r", + " function (appErr, appResonse) {\r", + " if(appErr) { console.log(\"Error in Pre-request:\", appErr); }\r", + " const appJson = appResonse.json();\r", + " if(!appJson.id) { console.log('Error in Pre-request: applicationId missing from response. Response is:', appJson); }\r", + " else {\r", + " pm.collectionVariables.set(\"OtherApplicationId\", appJson.id);\r", + " }\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{ApplicationVendorId}}/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{ApplicationVendorId}}", + "applications" + ] + } + }, + "response": [] + }, + { + "name": "Applications", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"PUT Application: Response result includes updated application\", function () {\r", + " pm.expect(result.applicationName).to.equal(\"Updated Application Name\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi ODS Admin App\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceId).to.not.equal(0);\r", + "});\r", + "\r", + "pm.test(\"PUT Application: Response result does not include application key and secret\", function () {\r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application: Request updated Application/Vendor relationship\", function () {\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors/${pm.collectionVariables.get(\"ApplicationVendorId\")}/applications`,\r", + " method: 'GET',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Application Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Application User\",\r", + " \"contactEmailAddress\": \"application@example.com\"\r", + " }), \r", + " }\r", + " }, \r", + " function (err, response) {\r", + " if(err) { console.log(\"Error in test request:\", err); }\r", + " if(response.code != 200) { console.log('Error in test request. Response is:', response); }\r", + " const results = response.json();\r", + " pm.expect(results.length).to.equal(0);\r", + " });\r", + "});\r", + "\r", + "const PutApplicationSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceId\"\r", + " ]\r", + " }\r", + "\r", + "pm.test(\"PUT Application: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PutApplicationSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Updated Application Name\",\r\n \"vendorId\": {{OtherApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi ODS Admin App\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [1234],\r\n \"odsInstanceId\": {{ODSInstanceId}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT Application Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"ApplicationName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ClaimSetName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"EducationOrganizationIds\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"\",\r\n \"vendorId\": {{OtherApplicationVendorId}},\r\n \"claimSetName\": \"\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [],\r\n \"odsInstanceId\": {{ODSInstanceId}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Vendor", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application Invalid Vendor: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT Application Invalid Vendor: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid Vendor: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid Vendor: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.VendorId.length).to.equal(1);\r", + " pm.expect(response.errors.VendorId[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Updated Application Name\",\r\n \"vendorId\": 9999,\r\n \"claimSetName\": \"Ed-Fi ODS Admin App\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [1234],\r\n \"odsInstanceId\": 10\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Profile", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application Invalid Profile: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"PUT Application Invalid Profile: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid Profile: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid Profile: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ProfileIds.length).to.equal(1);\r", + " pm.expect(response.errors.ProfileIds[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Updated Application Name\",\r\n \"vendorId\": {{OtherApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi ODS Admin App\",\r\n \"profileIds\": [9999],\r\n \"educationOrganizationIds\": [1234],\r\n \"odsInstanceId\": {{ODSInstanceId}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Reset Credential", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ResetCredentials: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"PUT ResetCredentials: Response result includes application key and secret\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + " pm.expect(result).to.have.property(\"key\");\r", + " pm.expect(result).to.have.property(\"secret\");\r", + "});\r", + "\r", + "const PutResetCredentialsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"key\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"secret\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"key\",\r", + " \"secret\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"PUT Reset Credentials: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PutResetCredentialsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}", + "reset-credential" + ] + } + }, + "response": [] + }, + { + "name": "Applications", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DELETE Applications: Response matches success format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE Applications: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"application\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "\r", + "const DeleteApplicationsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"DELETE Applications: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(DeleteApplicationsSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Application NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Application NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Application NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"application\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Reset Credential - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Reset Credential NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT Reset Credential NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT Reset Credential NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"application\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}", + "reset-credential" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT Application NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"application\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationId\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceId\": {{ODSInstanceId}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL Application NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"DEL Application NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DEL Application NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"application\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"ApplicationVendorId\");\r", + "pm.collectionVariables.unset(\"CreatedApplicationId\");\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "ClaimSets", + "item": [ + { + "name": "ClaimSets", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"ClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " pm.response.to.be.header(\"Location\", `/claimsets/${result.id}`);\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets: Response result includes claimSet key and secret\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + "});\r", + "\r", + "if(result.id) {\r", + " pm.collectionVariables.set(\"CreatedClaimSetId\", result.id);\r", + "}\r", + "\r", + "const PostClaimSetsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"resourceClaims\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"read\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"create\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"update\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"delete\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_defaultAuthStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"authStrategyId\",\r", + " \"authStrategyName\",\r", + " \"displayName\",\r", + " \"isInheritedFromParent\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"authStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"read\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"create\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"update\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"delete\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_defaultAuthStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"authStrategyId\",\r", + " \"authStrategyName\",\r", + " \"displayName\",\r", + " \"isInheritedFromParent\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"authStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"read\",\r", + " \"create\",\r", + " \"update\",\r", + " \"delete\",\r", + " \"_defaultAuthStrategiesForCRUD\",\r", + " \"authStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"read\",\r", + " \"create\",\r", + " \"update\",\r", + " \"delete\",\r", + " \"_defaultAuthStrategiesForCRUD\",\r", + " \"authStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"resourceClaims\",\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST ClaimSets: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostClaimSetsSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test ClaimSet {{ClaimSetGUID}}\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningStandard\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"\",\r\n \"resourceClaims\": []\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid JSON", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Json: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Json: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Json: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Json: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \r\n\t\"noname\": \"Not-Valid\",\r\n \"window\": {\r\n \"title\": \"Sample Konfabulator Widget\",\r\n \"name\": \"main_window\",\r\n \"width\": 500,\r\n \"height\": 500\r\n },\r\n \"image\": { \r\n \"src\": \"Images/Sun.png\",\r\n \"name\": \"sun1\",\r\n \"hOffset\": 250,\r\n \"vOffset\": 250,\r\n \"alignment\": \"center\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Existing ClaimSet Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OtherClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimsets`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"name\": `Other Test ClaimSet ${pm.collectionVariables.get(\"OtherClaimSetGUID\")}`,\r", + " \"resourceClaims\": []\r", + " }), \r", + " }\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSetJson = claimSetResponse.json();\r", + " if(!claimSetJson.id) { console.log('Error in Pre-request: claimset ID missing from response. Response is:', claimSetJson); }\r", + " pm.collectionVariables.set(\"OtherExistingClaimSetId\", claimSetJson.id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Name.length).to.equal(1);\r", + " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Other Test ClaimSet {{OtherClaimSetGUID}}\",\r\n \"resourceClaims\": []\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Wrong Resource Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Resource Name: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Resource Name: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Resource Name: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Resource Name: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(2);\r", + " [\"not in the system\", \"educationStandards-123\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " });\r", + " [\"not in the system\", \"learningStandard-123\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[1]).to.contain(substring);\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"ClaimSet-WithWrongResource\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards-123\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningStandard-123\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Wrong Parent Child Relationship", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Parent Child Relationship: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Parent Child Relationship: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Parent Child Relationship: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Parent Child Relationship: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(1);\r", + " [\"Child resource: 'academicSubjectDescriptor'\", \"wrong parent resource\", \"parent resource is: 'systemDescriptors'\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " });\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Wrong-Parent-Child-Relation\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Resource Duplication", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Resource Duplcation: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Resource Duplcation: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Resource Duplcation: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Resource Duplcation: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(1);\r", + " [\"Only unique resource claims\", \"duplicate resource: 'learningStandard'\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Resource-Duplication\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningStandard\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n },\r\n\t\t {\r\n \"name\": \"learningStandard\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Add Action", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResourceClaimAction: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4,\r\n \"resourceClaimActions\": {\r\n \"create\": true,\r\n \"read\": true,\r\n \"update\": true,\r\n \"delete\": false\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaimActions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaimActions" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Add Action Validation Errors", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResourceClaimAction Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ResourceClaimAction Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ResourceClaimAction Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ResourceClaimAction Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(2);\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(\"which is not in the system\");\r", + " pm.expect(response.errors.ResourceClaims[1]).to.contain(\"have at least one action\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4000,\r\n \"resourceClaimActions\": {\r\n \"create\": false,\r\n \"read\": false,\r\n \"update\": false,\r\n \"delete\": false\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaimActions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaimActions" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Modify Action", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ResourceClaimAction: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4,\r\n \"resourceClaimActions\": {\r\n \"create\": true,\r\n \"read\": true,\r\n \"update\": false,\r\n \"delete\": false\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaimActions/4", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaimActions", + "4" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Modify Action ClaimSet not found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ResourceClaimAction Not Found: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ResourceClaimAction Not Found: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT ResourceClaimAction Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"claimset\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4,\r\n \"resourceClaimActions\": {\r\n \"create\": true,\r\n \"read\": true,\r\n \"update\": false,\r\n \"delete\": false\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/90000/resourceclaimActions/4", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "90000", + "resourceclaimActions", + "4" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Override Auth Strategy", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OverrideAuthStrategy: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"actionName\": \"create\",\r\n \"authStrategyName\": \"RelationshipsWithStudentsOnly\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaims/4/overrideauthstrategy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaims", + "4", + "overrideauthstrategy" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Override Auth Strategy Validation Errors", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OverrideAuthStrategy Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ResourceClaim.length).to.equal(1);\r", + " pm.expect(response.errors.ResourceClaim[0]).to.contain(\"doesn't exist\");\r", + " pm.expect(response.errors.AuthStrategyName.length).to.equal(1);\r", + " pm.expect(response.errors.AuthStrategyName[0]).to.contain(\"doesn't exist\");\r", + " pm.expect(response.errors.ActionName.length).to.equal(1);\r", + " pm.expect(response.errors.ActionName[0]).to.contain(\"doesn't exist\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"actionName\": \"NotExist\",\r\n \"authStrategyName\": \"RelationshipsWithStudentsOnlys\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaims/4000/overrideauthstrategy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaims", + "4000", + "overrideauthstrategy" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Reset Auth Strategies", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResetAuthStrategies: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaims/4/resetauthstrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaims", + "4", + "resetauthstrategies" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Reset Auth Strategies Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResetAuthStrategies: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaims/40000/resetauthstrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaims", + "40000", + "resetauthstrategies" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Delete", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE ResourceClaimOnClaimSet: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaims/4", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaims", + "4" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Delete Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE ResourceClaimOnClaimSet: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaims/4000", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaims", + "4000" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets/Copy- Invalid Existing ClaimSet Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OtherClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimsets`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"name\": `Other Test ClaimSet ${pm.collectionVariables.get(\"OtherClaimSetGUID\")}`,\r", + " \"resourceClaims\": []\r", + " }), \r", + " }\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSetJson = claimSetResponse.json();\r", + " if(!claimSetJson.id) { console.log('Error in Pre-request: claimset ID missing from response. Response is:', claimSetJson); }\r", + " pm.collectionVariables.set(\"OtherExistingClaimSetId\", claimSetJson.id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Name.length).to.equal(1);\r", + " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Other Test ClaimSet {{OtherClaimSetGUID}}\",\r\n \"resourceClaims\": []\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets/Copy", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OtherClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimsets`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"name\": `Other Test ClaimSet ${pm.collectionVariables.get(\"OtherClaimSetGUID\")}`,\r", + " \"resourceClaims\": [{\r", + " \"name\": \"educationStandards\",\r", + " \"read\": true,\r", + " \"create\": true,\r", + " \"update\": true,\r", + " \"delete\": true,\r", + " \"defaultAuthStrategiesForCRUD\": [\r", + " {\r", + " \"authStrategyName\": \"NamespaceBased\",\r", + " \"isInheritedFromParent\": false\r", + " },\r", + " {\r", + " \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r", + " \"isInheritedFromParent\": false\r", + " },\r", + " {\r", + " \"authStrategyName\": \"NamespaceBased\",\r", + " \"isInheritedFromParent\": false\r", + " },\r", + " {\r", + " \"authStrategyName\": \"NamespaceBased\",\r", + " \"isInheritedFromParent\": false\r", + " }\r", + " ],\r", + " \"authStrategyOverridesForCRUD\": [\r", + " null,\r", + " null,\r", + " null,\r", + " null\r", + " ]\r", + " }]\r", + " })\r", + " }\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSetJson = claimSetResponse.json();\r", + " if(!claimSetJson.id) { console.log('Error in Pre-request: claimset ID missing from response. Response is:', claimSetJson); }\r", + " pm.collectionVariables.set(\"OtherExistingClaimSetId\", claimSetJson.id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " pm.response.to.be.header(\"Location\", `/claimsets/${result.id}`);\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets: Response result claimset has expected name and resource claims\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + " pm.expect(result).to.have.property(\"name\");\r", + " pm.expect(result.name).contains(\"Copied ClaimSet from\");\r", + " pm.expect(result.resourceClaims).to.not.be.empty;\r", + " const resourceclaimexists = result.resourceClaims.any(r => r.name === \"educationStandards\")\r", + " pm.expect(resourceclaimexists).to.equal(true);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Copied ClaimSet from {{OtherExistingClaimSetId}}\",\r\n \"originalid\": {{OtherExistingClaimSetId}}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/copy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "copy" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets/Copy- Invalid ClaimSet Id", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "function randomIntFromInterval(min, max) { // min and max included \r", + " return Math.floor(Math.random() * (max - min + 1) + min)\r", + "}\r", + "\r", + "const rndInt = randomIntFromInterval(450, 783)\r", + "pm.collectionVariables.set(\"NotExistClaimSetId\", pm.variables.replaceIn(rndInt));\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSet NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSet NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response.title).to.contains(\"Not found\");\r", + " pm.expect(response.title).to.contains(\"claimset\"); \r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Claim set does not exists\",\r\n \"originalid\": {{NotExistClaimSetId}}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/copy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "copy" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSets: Response result includes claimsets\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfClaimSet = results.map(\r", + " function(claimSet) { return claimSet.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedClaimSetId\"));\r", + "\r", + " const result = results[indexOfClaimSet];\r", + " pm.expect(result.name).to.equal(`Test ClaimSet ${pm.collectionVariables.get(\"ClaimSetGUID\")}`);\r", + " pm.expect(result._isSystemReserved).to.equal(false);\r", + " pm.expect(result._applications).to.be.empty;\r", + "});\r", + "\r", + "const GetClaimSetsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSet: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSetsId: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSetsId: Response result matches claimset\", function () {\r", + " const claimSetId = pm.collectionVariables.get(\"CreatedClaimSetId\");\r", + " \r", + " pm.expect(result.id).to.equal(claimSetId);\r", + " pm.expect(result.name).to.equal(`Test ClaimSet ${pm.collectionVariables.get(\"ClaimSetGUID\")}`);\r", + " pm.expect(result._isSystemReserved).to.equal(false);\r", + " pm.expect(result._applications).to.be.empty;\r", + " pm.expect(result.resourceClaims).to.not.be.empty;\r", + " const educationStandardsResourceClaim = result.resourceClaims.find(r => r.name === \"educationStandards\")\r", + " pm.expect(educationStandardsResourceClaim).to.be.an(\"object\", \"The educationStandards resource claim was not found.\")\r", + " const academicSubjectDescriptorResourceClaim = result.resourceClaims.find(r => r.name === \"academicSubjectDescriptor\")\r", + " pm.expect(academicSubjectDescriptorResourceClaim).to.be.an(\"object\", \"The academicSubjectDescriptor resource claim was not found.\")\r", + "});\r", + "\r", + "const GetClaimSetsIdSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"resourceClaims\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"read\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"create\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"update\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"delete\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_defaultAuthStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"authStrategyId\",\r", + " \"authStrategyName\",\r", + " \"displayName\",\r", + " \"isInheritedFromParent\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"authStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"read\",\r", + " \"create\",\r", + " \"update\",\r", + " \"delete\",\r", + " \"_defaultAuthStrategiesForCRUD\",\r", + " \"authStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"resourceClaims\",\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSetId: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsIdSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets: Response result includes updated claimset\", function () {\r", + " pm.expect(result.name).to.equal(\"Updated Test ClaimSet\");\r", + " pm.expect(result._isSystemReserved).to.equal(false);\r", + " pm.expect(result._applications).to.be.empty;\r", + " pm.expect(result.resourceClaims).to.not.be.empty;\r", + " const educationStandardsResourceClaim = result.resourceClaims.find(r => r.name === \"educationStandards\")\r", + " pm.expect(educationStandardsResourceClaim).to.be.an(\"object\", \"The educationStandards resource claim was not found.\")\r", + " const academicSubjectDescriptorResourceClaim = result.resourceClaims.any(r => r.name === \"academicSubjectDescriptor\")\r", + " pm.expect(academicSubjectDescriptorResourceClaim).to.equal(false);\r", + "});\r", + "\r", + "const PutClaimSetsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"resourceClaims\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"read\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"create\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"update\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"delete\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_defaultAuthStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"authStrategyId\",\r", + " \"authStrategyName\",\r", + " \"displayName\",\r", + " \"isInheritedFromParent\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"authStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"read\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"create\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"update\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"delete\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_defaultAuthStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"authStrategyId\",\r", + " \"authStrategyName\",\r", + " \"displayName\",\r", + " \"isInheritedFromParent\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"authStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"read\",\r", + " \"create\",\r", + " \"update\",\r", + " \"delete\",\r", + " \"_defaultAuthStrategiesForCRUD\",\r", + " \"authStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"read\",\r", + " \"create\",\r", + " \"update\",\r", + " \"delete\",\r", + " \"_defaultAuthStrategiesForCRUD\",\r", + " \"authStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"resourceClaims\",\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"PUT ClaimSets: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PutClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"Updated Test ClaimSet\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningStandard\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n } \r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"\",\r\n \"resourceClaims\": []\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid JSON", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid Json: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Json: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Json: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Json: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}}, \r\n\t\"noname\": \"Not-Valid\",\r\n \"window\": {\r\n \"title\": \"Sample Konfabulator Widget\",\r\n \"name\": \"main_window\",\r\n \"width\": 500,\r\n \"height\": 500\r\n },\r\n \"image\": { \r\n \"src\": \"Images/Sun.png\",\r\n \"name\": \"sun1\",\r\n \"hOffset\": 250,\r\n \"vOffset\": 250,\r\n \"alignment\": \"center\"\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Existing ClaimSet Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid Existing ClaimSets: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Existing ClaimSets: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Existing ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Existing ClaimSets: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Name.length).to.equal(1);\r", + " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"Other Test ClaimSet {{OtherClaimSetGUID}}\",\r\n \"resourceClaims\": []\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Wrong Resource Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid Wrong Resource: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Wrong Resource: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Wrong Resource: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Wrong Resource: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(2);\r", + " [\"not in the system\", \"educationStandards-123\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " });\r", + " [\"not in the system\", \"learningStandard-123\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[1]).to.contain(substring);\r", + " });\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"ClaimSet-WithWrongResource\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards-123\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningStandard-123\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Wrong Parent Child Relationship", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid Wrong Parent Child: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Wrong Parent Child: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Wrong Parent Child: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Wrong Parent Child: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(1);\r", + " [\"Child resource: 'academicSubjectDescriptor'\", \"wrong parent resource\", \"parent resource is: 'systemDescriptors'\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " });\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"Wrong-Parent-Child-Relation\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Resource Duplication", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid Resource Duplication: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Resource Duplication: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Resource Duplication: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Resource Duplication: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(1);\r", + " [\"Only unique resource claims\", \"duplicate resource: 'learningStandard'\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " });\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"Resource-Duplication\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningStandard\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n },\r\n\t\t {\r\n \"name\": \"learningStandard\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - System Reserved", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimsets?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " }\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSets = claimSetResponse.json();\r", + " if(!claimSets) { console.log('Error in Pre-request: ClaimSets missing from response.'); }\r", + " const systemReservedClaimSetIds = claimSets.map(\r", + " function(claimSet) { \r", + " if(claimSet._isSystemReserved)\r", + " {\r", + " return claimSet.id;\r", + " } \r", + " }\r", + " );\r", + " if(!systemReservedClaimSetIds) { console.log('Error in Pre-request: System Reserved claimset IDs not found. Response is:', claimSets); }\r", + " pm.collectionVariables.set(\"SystemReservedClaimSetId\", systemReservedClaimSetIds[0]);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets System Reserved: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets System Reserved: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets System Reserved: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets System Reserved: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"id\"].length).to.equal(1);\r", + " [\"AB Connect\", \"system reserved\"].forEach((substring) => {\r", + " pm.expect(response.errors.id[0]).to.contain(substring);\r", + " });\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{SystemReservedClaimSetId}},\r\n \"name\": \"Update System Reserved ClaimSet\",\r\n \"resourceClaims\": []\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{SystemReservedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{SystemReservedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DELETE ClaimSets: Response matches success format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"claimset\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "\r", + "const DeleteClaimSetsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"DELETE ClaimSet: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(DeleteClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - System Reserved", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL ClaimSets System Reserved: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DEL ClaimSets System Reserved: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSets System Reserved: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSets System Reserved: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"id\"].length).to.equal(1);\r", + " [\"AB Connect\", \"system reserved\"].forEach((substring) => {\r", + " pm.expect(response.errors.id[0]).to.contain(substring);\r", + " });\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"SystemReservedClaimSetId\");\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{SystemReservedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{SystemReservedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - With Applications", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL ClaimSets With Application: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DEL ClaimSets With Application: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSets With Application: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSets With Application: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"id\"].length).to.equal(1);\r", + " [\"Cannot delete\", \"associated application\"].forEach((substring) => {\r", + " pm.expect(response.errors.id[0]).to.contain(substring);\r", + " });\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"OtherApplicationId\");\r", + "pm.collectionVariables.unset(\"OtherApplicationVendorId\");\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/applications/${pm.collectionVariables.get(\"OtherApplicationId\")}`,\r", + " method: 'PUT',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"applicationName\": \"ClaimSet Test Vendor Application\",\r", + " \"vendorId\": pm.collectionVariables.get(\"OtherApplicationVendorId\"),\r", + " \"claimSetName\": `Other Test ClaimSet ${pm.collectionVariables.get(\"OtherClaimSetGUID\")}`,\r", + " \"profileIds\": [],\r", + " \"educationOrganizationIds\": [ 255901 ],\r", + " \"odsInstanceId\": 1\r", + " }),\r", + " }\r", + " }, \r", + " function (appErr, appResonse) {\r", + " if(appErr) { console.log(\"Error in Pre-request:\", appErr); }\r", + " const appJson = appResonse.json();\r", + " if(!appJson.id) { console.log('Error in Pre-request: applicationId missing from response. Response is:', appJson); }\r", + "}); " + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{OtherExistingClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{OtherExistingClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSet NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET ClaimSet NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"claimset\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedClaimSetId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSet NotFound: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSet NotFound: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"Edited-ClaimSet\",\r\n \"resourceClaims\": []\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL ClaimSet NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSet NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"claimset\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedClaimSetId\"));\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"CreatedClaimSetId\");\r", + "pm.collectionVariables.unset(\"OtherExistingClaimSetId\");\r", + "pm.collectionVariables.unset(\"ClaimSetGUID\");\r", + "pm.collectionVariables.unset(\"OtherClaimSetGUID\");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Resourceclaims", + "item": [ + { + "name": "Resourceclaims", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaims: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET ResourceClaims: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"parentId\");", + " pm.expect(response[0]).to.have.property(\"parentName\");", + " pm.expect(response[0]).to.have.property(\"children\");", + "});", + "", + "const GetResourceClaimsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"null\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaims: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsSchema);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceclaims", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceclaims" + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimsId: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET ResourceClaimsId: Response matches success format\", function () {", + " pm.expect(response).to.have.property(\"id\");", + " pm.expect(response).to.have.property(\"name\");", + " pm.expect(response).to.have.property(\"parentId\");", + " pm.expect(response).to.have.property(\"parentName\");", + " pm.expect(response).to.have.property(\"children\");", + "});", + "", + "const GetResourceClaimsIdSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"null\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimsId: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsIdSchema);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceclaims/1", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceclaims", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims by ID Children", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimsId Children: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET ResourceClaimsId Children: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"parentId\");", + " pm.expect(response[0]).to.have.property(\"parentName\");", + " pm.expect(response[0]).to.have.property(\"children\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceclaims/1/children", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceclaims", + "1", + "children" + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims Children - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Resourceclaim by Id Children NotFound: Status code is Not Found\", function () {", + " pm.response.to.have.status(404);", + "});", + "", + "pm.test(\"GET Resourceclaim by Id Children NotFound: Response matches error format\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response).to.have.property(\"title\");", + "});", + "", + "pm.test(\"GET Resourceclaim by Id Children NotFound: Response title is helpful and accurate\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response.title).to.contain(\"Not found\");", + " pm.expect(response.title).to.contain(\"resourceclaim\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceclaims/000/children", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceclaims", + "000", + "children" + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims -Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Resourceclaim NotFound: Status code is Not Found\", function () {", + " pm.response.to.have.status(404);", + "});", + "", + "pm.test(\"GET Resourceclaim NotFound: Response matches error format\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response).to.have.property(\"title\");", + "});", + "", + "pm.test(\"GET Resourceclaim NotFound: Response title is helpful and accurate\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response.title).to.contain(\"Not found\");", + " pm.expect(response.title).to.contain(\"resourceclaim\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceclaims/00", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceclaims", + "00" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "AuthStrategies", + "item": [ + { + "name": "Authstrategies", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET AuthStrategies: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"GET AuthStrategies: Response matches success format\", function () {\r", + " pm.expect(response[0]).to.have.property(\"authStrategyId\");\r", + " pm.expect(response[0]).to.have.property(\"authStrategyName\");\r", + " pm.expect(response[0]).to.have.property(\"displayName\");\r", + "});\r", + "\r", + "const GetAuthStrategiesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"authStrategyId\",\r", + " \"authStrategyName\",\r", + " \"displayName\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET AuthStrategies: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetAuthStrategiesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/authstrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "authstrategies" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Profiles", + "item": [ + { + "name": "Profiles", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " const id = pm.response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"CreatedProfileId\", id);\r", + " }\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Name\"][0].toLowerCase()).to.contain(\"'name' must not be empty\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Invalid definition xml", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Definition\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"test\");\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"readcontenttype\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Name mismatch", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Definition\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"match with test-profile-123\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile-123\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ] + } + }, + "response": [] + }, + { + "name": "Profiles", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profiles: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result includes profiles\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetProfilesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Profile: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetProfilesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/Profiles/?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Profile by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ProfileById: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET ProfileById: Response result matches profile\", function () {\r", + " const result = pm.response.json();\r", + " pm.expect(result.name).to.equal(`Test-Profile`);\r", + " pm.expect(result.definition).to.not.be.empty; \r", + "});\r", + "\r", + "const GetProfileIdSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"definition\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " }\r", + "}\r", + "\r", + "pm.test(\"GET ProfileById: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetProfileIdSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profile by ID - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profile NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"profile\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistProfileId\"));\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/profiles/{{NotExistProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles", + "{{NotExistProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Profiles: Status code is Created\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/profiles/${pm.collectionVariables.get(\"CreatedProfileId\")}`,\r", + " method: 'GET',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " }\r", + "},\r", + "function (profileErr, profileResponse) {\r", + " if(profileErr) \r", + " { \r", + " console.log(\"Error :\", profileErr); \r", + " } \r", + " const updatedProfileJson = profileResponse.json();\r", + " pm.test(\"PUT Profiles: Response includes updated profile\", function () {\r", + " pm.expect(updatedProfileJson.name).to.equal(\"Updated-Test-Profile\");\r", + " pm.expect(updatedProfileJson.definition).to.not.be.empty;\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Updated-Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profile NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"profile\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistProfileId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Not-Found\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/profiles/{{NotExistProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles", + "{{NotExistProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Name\"][0].toLowerCase()).to.contain(\"'name' must not be empty\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Invalid definition xml", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Definition\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"test\");\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"readcontenttype\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Updated-Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Name mismatch", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Definition\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"match with test-profile-123\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile-123\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/Profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE Profile: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/profiles/{{CreatedProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles", + "{{CreatedProfileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Profiles - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profile NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"profile\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistProfileId\"));\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/profiles/{{NotExistProfileId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "profiles", + "{{NotExistProfileId}}" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function randomIntFromInterval(min, max) { // min and max included ", + " return Math.floor(Math.random() * (max - min + 1) + min)", + "}", + "", + "const rndInt = randomIntFromInterval(450, 783)", + "pm.collectionVariables.set(\"NotExistProfileId\", pm.variables.replaceIn(rndInt));" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "Actions", + "item": [ + { + "name": "Authstrategies", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Actions: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"GET Actions: Response matches success format\", function () {\r", + " pm.expect(response[0]).to.have.property(\"id\");\r", + " pm.expect(response[0]).to.have.property(\"name\");\r", + " pm.expect(response[0]).to.have.property(\"uri\");\r", + "});\r", + "\r", + "const GetActionsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"uri\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"uri\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Actions: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetActionsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/actions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "actions" + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "v1", + "item": [ + { + "name": "OdsInstances", + "item": [ + { + "name": "OdsInstances", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstances: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET OdsInstances: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"odsInstanceId\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"instanceType\");", + "});", + "", + "const GetOdsInstancesSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"odsInstanceId\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"instanceType\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"odsInstanceId\",", + " \"name\",", + " \"instanceType\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET OdsInstances: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesSchema);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/odsinstances?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsinstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstances NotFound: Status code is Not Found\", function () {", + " pm.response.to.have.status(404);", + "});", + "", + "pm.test(\"GET OdsInstances NotFound: Response matches error format\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response).to.have.property(\"title\");", + "});", + "", + "pm.test(\"GET OdsInstances NotFound: Response title is helpful and accurate\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response.title).to.contain(\"Not found\");", + " pm.expect(response.title).to.contain(\"odsinstance\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/odsinstances/0", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsinstances", + "0" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstancesID: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET OdsInstancesID: Response matches success format\", function () {", + " pm.expect(response).to.have.property(\"odsInstanceId\");", + " pm.expect(response).to.have.property(\"name\");", + " pm.expect(response).to.have.property(\"instanceType\");", + "});", + "", + "const GetOdsInstancesIdSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"odsInstanceId\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"instanceType\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"odsInstanceId\",", + " \"name\",", + " \"instanceType\"", + " ]", + "}", + "", + "pm.test(\"GET OdsInstancesID: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesIdSchema);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/odsinstances/{{ODSInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsinstances", + "{{ODSInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances by ID Application", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstancesIDApplication: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET OdsInstancesIDApplication: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"applicationName\");", + " pm.expect(response[0]).to.have.property(\"claimSetName\");", + " pm.expect(response[0]).to.have.property(\"educationOrganizationIds\");", + " pm.expect(response[0]).to.have.property(\"vendorId\");", + " pm.expect(response[0]).to.have.property(\"profileIds\");", + " pm.expect(response[0]).to.have.property(\"odsInstanceId\");", + "});", + "", + "const GetOdsInstancesIdApplication = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"applicationName\": {", + " \"type\": \"string\"", + " },", + " \"claimSetName\": {", + " \"type\": \"string\"", + " },", + " \"educationOrganizationIds\": {", + " \"type\": \"array\",", + " \"items\": {}", + " },", + " \"odsInstanceName\": {", + " \"type\": \"string\"", + " },", + " \"vendorId\": {", + " \"type\": \"integer\"", + " },", + " \"profileIds\": {", + " \"type\": \"array\",", + " \"items\": {}", + " },", + " \"odsInstanceId\": {", + " \"type\": \"integer\"", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"applicationName\",", + " \"claimSetName\",", + " \"educationOrganizationIds\",", + " \"vendorId\",", + " \"profileIds\",", + " \"odsInstanceId\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET OdsInstancesIdApplication: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesIdApplication);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/odsinstances/{{ODSInstanceId}}/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsinstances", + "{{ODSInstanceId}}", + "applications" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if(pm.request.auth && pm.request.auth.type === \"noauth\") {", + " return;", + "}", + "", + "let currentToken = pm.collectionVariables.get(\"TOKEN\");", + "if(currentToken) {", + " return;", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + " },", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: pm.variables.get(\"RegisteredClientId\")},", + " {key: 'client_secret', value: pm.variables.get(\"RegisteredClientSecret\")},", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "ODSInstanceId", + "value": "" + }, + { + "key": "NotExistClaimSetId", + "value": "" + }, + { + "key": "OtherApplicationVendorId", + "value": "" + }, + { + "key": "OtherApplicationId", + "value": "" + }, + { + "key": "ClaimSetGUID", + "value": "" + }, + { + "key": "CreatedClaimSetId", + "value": "" + }, + { + "key": "OtherClaimSetGUID", + "value": "" + }, + { + "key": "OtherExistingClaimSetId", + "value": "" + }, + { + "key": "RegisteredClientId", + "value": "" + }, + { + "key": "RegisteredClientSecret", + "value": "" + }, + { + "key": "ApplicationVendorId", + "value": "" + }, + { + "key": "CreatedApplicationId", + "value": "" + }, + { + "key": "FULL_ACCESS_TOKEN", + "value": "" + }, + { + "key": "TENANT_ACCESS_TOKEN", + "value": "" + }, + { + "key": "WORKER_PROCESS_TOKEN", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E.postman_collection.json new file mode 100644 index 000000000..3f9b4d4e0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E.postman_collection.json @@ -0,0 +1,7664 @@ +{ + "info": { + "_postman_id": "a5c2ba02-c52d-4df2-9f16-e3bd8b76d89e", + "name": "Admin API E2E 2.0", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Landing", + "item": [ + { + "name": "Landing Page", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Landing: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"GET Landing: Response includes expected properties\", function () {\r", + " pm.expect(response).to.have.property(\"version\");\r", + " pm.expect(response).to.have.property(\"build\");\r", + "});\r", + "\r", + "const GetSchemaLanding = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"version\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"build\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"version\",\r", + " \"build\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Landing: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetSchemaLanding);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}", + "host": [ + "{{API_URL}}" + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "noauth" + } + }, + { + "name": "User Management", + "item": [ + { + "name": "Register", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Register: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json().result;\r", + "\r", + "pm.test(\"POST Register: Response matches success format\", function () {\r", + " pm.expect(response.status).to.equal(200);\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"POST Register: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"client\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"registered\");\r", + "});\r", + "\r", + "const PostRegisterSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"status\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\",\r", + " \"status\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Register: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostRegisterSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"RegisteredClientId\", pm.variables.replaceIn('{{$guid}}'));\r", + "pm.collectionVariables.set(\"RegisteredClientSecret\", pm.variables.replaceIn('{{$guid}}'));\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ClientId", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "ClientSecret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "DisplayName", + "value": "Postman Test", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/register", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Register - Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Register Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Register Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"ClientId\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ClientSecret\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"DisplayName\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ClientId", + "value": "", + "type": "text" + }, + { + "key": "ClientSecret", + "value": "", + "type": "text" + }, + { + "key": "DisplayName", + "value": "", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/register", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Register - Already Exists", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Register Already Exists: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Register Already Exists: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Already Exists: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Register Already Exists: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ClientId.length).to.equal(1);\r", + " pm.expect(response.errors.ClientId[0]).to.contain(\"already exists\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "ClientId", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "ClientSecret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "DisplayName", + "value": "{{UserName}}", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/register", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "register" + ] + } + }, + "response": [] + }, + { + "name": "Token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token: Response includes token\", function () {\r", + " pm.expect(response).to.have.property(\"access_token\");\r", + " pm.expect(response).to.have.property(\"token_type\");\r", + " pm.expect(response).to.have.property(\"expires_in\");\r", + "\r", + " pm.expect(response[\"token_type\"]).to.equal(\"Bearer\");\r", + "});\r", + "\r", + "const PostTokenSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"access_token\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"token_type\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"expires_in\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"access_token\",\r", + " \"token_type\",\r", + " \"expires_in\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Token: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostTokenSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/full_access", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Invalid: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + " pm.expect(response).to.have.property(\"error_uri\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{$guid}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Invalid Grant Type", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Invalid Grant Type: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Invalid Grant Type: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + " pm.expect(response).to.have.property(\"error_uri\");\r", + "\r", + " pm.expect(response[\"error_description\"]).to.contain(\"grant_type\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{RegisteredClientSecret}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "authorization_code", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/full_access", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Invalid Scope", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Invalid Scope: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Invalid Scope: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + " pm.expect(response).to.have.property(\"error_uri\");\r", + "\r", + " pm.expect(response[\"error_description\"]).to.contain(\"scope\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{$guid}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "NOT_REAL/SCOPE", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Token - Incorrect Secret", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Token Incorrect Secret: Status code is Unauthorized\", function () {\r", + " pm.response.to.have.status(401);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Token Incorrect Secret: Response includes error message\", function () {\r", + " pm.expect(response).to.have.property(\"error\");\r", + " pm.expect(response).to.have.property(\"error_description\");\r", + " pm.expect(response).to.have.property(\"error_uri\");\r", + "\r", + " pm.expect(response[\"error_description\"]).to.contain(\"credentials\");\r", + " pm.expect(response[\"error_description\"]).to.contain(\"invalid\");\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"RegisteredClientId\");\r", + "pm.collectionVariables.unset(\"RegisteredClientSecret\");" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "client_id", + "value": "{{RegisteredClientId}}", + "type": "text" + }, + { + "key": "client_secret", + "value": "{{$guid}}", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + }, + { + "key": "scope", + "value": "edfi_admin_api/full_access", + "type": "text" + } + ] + }, + "url": { + "raw": "{{API_URL}}/connect/token", + "host": [ + "{{API_URL}}" + ], + "path": [ + "connect", + "token" + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "noauth" + } + }, + { + "name": "v2", + "item": [ + { + "name": "Vendors", + "item": [ + { + "name": "Vendors", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Vendors: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"POST Vendors: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " pm.response.to.be.header(\"Location\", `/vendors/${result.id}`);\r", + "});\r", + "\r", + "pm.test(\"POST Vendors: Response result includes vendor info\", function () {\r", + " pm.expect(result.company).to.equal(\"Test Company\");\r", + " pm.expect(result.namespacePrefixes).to.equal(\"uri://ed-fi.org\");\r", + " pm.expect(result.contactName).to.equal(\"Test User\");\r", + " pm.expect(result.contactEmailAddress).to.equal(\"test@test-ed-fi.org\");\r", + "});\r", + "\r", + "if(result.id) {\r", + " pm.collectionVariables.set(\"CreatedVendorId\", result.id);\r", + "}\r", + "\r", + "const PostVendorSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostVendorSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Test Company\",\r\n \"namespacePrefixes\": \"uri://ed-fi.org\",\r\n \"contactName\": \"Test User\",\r\n \"contactEmailAddress\": \"test@test-ed-fi.org\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ] + } + }, + "response": [ + { + "name": "Vendor with multiple namespaces", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": {{CompanyName}},\r\n \"namespacePrefixes\": \"uri://ed-fi.org,uri://academicbenchmarks.com\",\r\n \"contactName\": {{ContactName}},\r\n \"contactEmailAddress\": {{ContactEmail}}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "vendors" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Date", + "value": "Thu, 02 Jun 2022 23:13:53 GMT" + }, + { + "key": "Server", + "value": "Kestrel" + }, + { + "key": "Location", + "value": "/Vendors/2" + }, + { + "key": "Transfer-Encoding", + "value": "chunked" + } + ], + "cookie": [], + "body": "{\n \"result\": {\n \"vendorId\": 2,\n \"company\": \"Test Company\",\n \"namespacePrefixes\": \"uri://ed-fi.org,uri://academicbenchmarks.com\",\n \"contactName\": \"Test User\",\n \"contactEmailAddress\": \"test@test-ed-fi.org\"\n },\n \"status\": 201,\n \"title\": \"Vendor created successfully\"\n}" + } + ] + }, + { + "name": "Vendors - Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Vendors Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Vendors Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Company\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ContactName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ContactEmailAddress\"].length).to.equal(2);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"\",\r\n \"namespacePrefixes\": \"\",\r\n \"contactName\": \"\",\r\n \"contactEmailAddress\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ] + } + }, + "response": [] + }, + { + "name": "Vendors", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Vendors: Response result includes vendors\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfVendor = results.map(\r", + " function(vendor) { return vendor.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedVendorId\"));\r", + "\r", + " const result = results[indexOfVendor];\r", + " pm.expect(result.company).to.equal(\"Test Company\");\r", + " pm.expect(result.namespacePrefixes).to.equal(\"uri://ed-fi.org\");\r", + " pm.expect(result.contactName).to.equal(\"Test User\");\r", + " pm.expect(result.contactEmailAddress).to.equal(\"test@test-ed-fi.org\");\r", + "});\r", + "\r", + "const GetVendorsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetVendorsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Vendors by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendor ID: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET Vendor ID: Response result matches vendor\", function () {\r", + " const vendorId = pm.collectionVariables.get(\"CreatedVendorId\");\r", + " \r", + " pm.expect(result.id).to.equal(vendorId);\r", + " pm.expect(result.company).to.equal(\"Test Company\");\r", + " pm.expect(result.namespacePrefixes).to.equal(\"uri://ed-fi.org\");\r", + " pm.expect(result.contactName).to.equal(\"Test User\");\r", + " pm.expect(result.contactEmailAddress).to.equal(\"test@test-ed-fi.org\");\r", + "});\r", + "\r", + "const GetVendorIdSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET VendorsID: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetVendorIdSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"PUT Vendors: Response result includes updated vendor\", function () {\r", + " pm.expect(result.company).to.equal(\"Updated Test Company\");\r", + " pm.expect(result.namespacePrefixes).to.equal(\"uri://academicbenchmarks.com\");\r", + " pm.expect(result.contactName).to.equal(\"Updated User\");\r", + " pm.expect(result.contactEmailAddress).to.equal(\"updated@example.com\");\r", + "});\r", + "\r", + "const PutVendorsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"company\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"namespacePrefixes\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"contactEmailAddress\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"company\",\r", + " \"namespacePrefixes\",\r", + " \"contactName\",\r", + " \"contactEmailAddress\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"PUT Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PutVendorsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Updated Test Company\",\r\n \"namespacePrefixes\": \"uri://academicbenchmarks.com\",\r\n \"contactName\": \"Updated User\",\r\n \"contactEmailAddress\": \"updated@example.com\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Vendors Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT Vendors Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Vendors Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Vendors Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Company\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ContactName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ContactEmailAddress\"].length).to.equal(2);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"\",\r\n \"contactName\": \"\",\r\n \"contactEmailAddress\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE Vendors: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DELETE Vendors: Response matches success format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE Vendors: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"vendor\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "\r", + "const DeleteVendorSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"DELETE Vendors: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(DeleteVendorSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Vendors NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Vendors NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Vendors NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"vendor\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedVendorId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Vendors NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT Vendors NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT Vendors NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"vendor\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedVendorId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"company\": \"Updated Test Company\",\r\n \"namespacePrefixes\": \"uri://academicbenchmarks.com\",\r\n \"contactName\": \"Updated User\",\r\n \"contactEmailAddress\": \"updated@example.com\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + }, + { + "name": "Vendors - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL Vendors NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"DEL Vendors NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DEL Vendors NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"vendor\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedVendorId\"));\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"CreatedVendorId\");\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Application", + "item": [ + { + "name": "Applications", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Application Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Application User\",\r", + " \"contactEmailAddress\": \"application@example.com\"\r", + " }), \r", + " }\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json.id) { console.log('Error in Pre-request: vendorID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ApplicationVendorId\", json.id);\r", + " }\r", + "});\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v1/odsinstances?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " }\r", + "}, \r", + "function (err, response) {\r", + " if(err) { console.log(\"Error in Pre-request:\", err); }\r", + " const json = response.json();\r", + " if(!json[0].odsInstanceId) { console.log('Error in Pre-request: odsInstancesID missing from response. Response is:', json); }\r", + " else {\r", + " pm.collectionVariables.set(\"ODSInstanceId\", json[0].odsInstanceId);\r", + " }\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "\r", + "pm.test(\"POST Applications: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " pm.response.to.be.header(\"Location\", `/applications/${result.id}`);\r", + "});\r", + "\r", + "pm.test(\"POST Applications: Response result includes application key and secret\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + " pm.expect(result).to.have.property(\"key\");\r", + " pm.expect(result).to.have.property(\"secret\");\r", + "});\r", + "\r", + "if(result.id) {\r", + " pm.collectionVariables.set(\"CreatedApplicationId\", result.id);\r", + "}\r", + "\r", + "const PostApplicationSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"key\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"secret\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"key\",\r", + " \"secret\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST Applications: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostApplicationSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceId\": {{ODSInstanceId}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Applications Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"ApplicationName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ClaimSetName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"EducationOrganizationIds\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"OdsInstanceId\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [],\r\n \"odsInstanceId\": 0\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Vendor", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications Invalid Vendor: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Applications Invalid Vendor: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid Vendor: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid Vendor: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.VendorId.length).to.equal(1);\r", + " pm.expect(response.errors.VendorId[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": 9999,\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceId\": 10\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Profile", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications Invalid Profile: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"POST Applications Invalid Profile: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid Profile: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid Profile: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ProfileIds.length).to.equal(1);\r", + " pm.expect(response.errors.ProfileIds[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [ 9999 ],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceId\": {{ODSInstanceId}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid OdsInstance", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Applications Invalid ODSInstance: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"POST Applications Invalid ODSInstance: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid ODSInstance: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Applications Invalid ODSInstance: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.OdsInstanceId.length).to.equal(1);\r", + " pm.expect(response.errors.OdsInstanceId[0]).to.contain(\"provide valid ods instance id.\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [ 9999 ],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceId\": 0\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } + }, + "response": [] + }, + { + "name": "Applications", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Applications: Response result includes applications\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfApplication = results.map(\r", + " function(application) { return application.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "\r", + " const result = results[indexOfApplication];\r", + " pm.expect(result.applicationName).to.equal(\"Test Application\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceid).to.not.equal(0);\r", + "});\r", + "\r", + "pm.test(\"GET Applications: Response results do not include key or secret\", function () {\r", + " results.forEach(function(result, i) {\r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + " });\r", + "});\r", + "\r", + "const GetApplicationsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceId\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Applications: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApplicationsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Applications by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ApplicationID: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET ApplicationID: Response result matches application\", function () {\r", + " const applicationId = pm.collectionVariables.get(\"CreatedApplicationId\");\r", + " \r", + " pm.expect(result.id).to.equal(applicationId);\r", + " pm.expect(result.applicationName).to.equal(\"Test Application\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceId).to.not.equal(0);\r", + "});\r", + "\r", + "pm.test(\"GET ApplicationID: Response result does not include key or secret\", function () { \r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + "});\r", + "\r", + "const GetApplicationId = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceId\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ApplicationId: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApplicationId);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications by Vendor", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Application by Vendor: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET Application by Vendor: Response result includes applications\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfApplication = results.map(\r", + " function(application) { return application.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "\r", + " const result = results[indexOfApplication];\r", + " pm.expect(result.applicationName).to.equal(\"Test Application\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi Sandbox\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceId).to.not.equal(0);\r", + "});\r", + "\r", + "pm.test(\"GET Application by Vendor: Response result is filtered by vendor\", function () {\r", + " const resultApplicationIds = results.map(\r", + " function(application) { return application.id; }\r", + " );\r", + "\r", + " pm.expect(resultApplicationIds).to.contain(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + " pm.expect(resultApplicationIds).to.not.contain(pm.collectionVariables.get(\"OtherApplicationId\"));\r", + "});\r", + "\r", + "pm.test(\"GET Application by Vendor: Response results do not include key or secret\", function () {\r", + " results.forEach(function(result, i) {\r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + " });\r", + "});\r", + "\r", + "const GetApplicationByVendor = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceId\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Application by Vendor: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetApplicationByVendor);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Other Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Other Application User\",\r", + " \"contactEmailAddress\": \"otherapplication@example.com\"\r", + " }), \r", + " }\r", + "},\r", + "function (vendorErr, vendorResponse) {\r", + " if(vendorErr) { console.log(\"Error in Pre-request:\", vendorErr); }\r", + " const vendorJson = vendorResponse.json();\r", + " if(!vendorJson.id) { console.log('Error in Pre-request: vendorID missing from response. Response is:', vendorJson); }\r", + " pm.collectionVariables.set(\"OtherApplicationVendorId\", vendorJson.id);\r", + "\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/applications`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"applicationName\": \"Other Vendor Application\",\r", + " \"vendorId\": pm.collectionVariables.get(\"OtherApplicationVendorId\"),\r", + " \"claimSetName\": \"Ed-Fi Sandbox\",\r", + " \"profileIds\": [],\r", + " \"educationOrganizationIds\": [ 255901 ],\r", + " \"odsInstanceId\": 1\r", + " }),\r", + " }\r", + " }, \r", + " function (appErr, appResonse) {\r", + " if(appErr) { console.log(\"Error in Pre-request:\", appErr); }\r", + " const appJson = appResonse.json();\r", + " if(!appJson.id) { console.log('Error in Pre-request: applicationId missing from response. Response is:', appJson); }\r", + " else {\r", + " pm.collectionVariables.set(\"OtherApplicationId\", appJson.id);\r", + " }\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/vendors/{{ApplicationVendorId}}/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{ApplicationVendorId}}", + "applications" + ] + } + }, + "response": [] + }, + { + "name": "Applications", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"PUT Application: Response result includes updated application\", function () {\r", + " pm.expect(result.applicationName).to.equal(\"Updated Application Name\");\r", + " pm.expect(result.claimSetName).to.equal(\"Ed-Fi ODS Admin App\");\r", + " pm.expect(result.educationOrganizationIds.length).to.equal(1);\r", + " pm.expect(result.profileIds.length).to.equal(0);\r", + " pm.expect(result.odsInstanceId).to.not.equal(0);\r", + "});\r", + "\r", + "pm.test(\"PUT Application: Response result does not include application key and secret\", function () {\r", + " pm.expect(result).to.not.have.property(\"key\");\r", + " pm.expect(result).to.not.have.property(\"secret\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application: Request updated Application/Vendor relationship\", function () {\r", + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/vendors/${pm.collectionVariables.get(\"ApplicationVendorId\")}/applications`,\r", + " method: 'GET',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"company\": \"Application Company\",\r", + " \"namespacePrefixes\": \"uri://ed-fi.org\",\r", + " \"contactName\": \"Application User\",\r", + " \"contactEmailAddress\": \"application@example.com\"\r", + " }), \r", + " }\r", + " }, \r", + " function (err, response) {\r", + " if(err) { console.log(\"Error in test request:\", err); }\r", + " if(response.code != 200) { console.log('Error in test request. Response is:', response); }\r", + " const results = response.json();\r", + " pm.expect(results.length).to.equal(0);\r", + " });\r", + "});\r", + "\r", + "const PutApplicationSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"applicationName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"claimSetName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"educationOrganizationIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"vendorId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"profileIds\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " },\r", + " \"odsInstanceId\": {\r", + " \"type\": \"integer\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"applicationName\",\r", + " \"claimSetName\",\r", + " \"educationOrganizationIds\",\r", + " \"vendorId\",\r", + " \"profileIds\",\r", + " \"odsInstanceId\"\r", + " ]\r", + " }\r", + "\r", + "pm.test(\"PUT Application: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PutApplicationSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Updated Application Name\",\r\n \"vendorId\": {{OtherApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi ODS Admin App\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [1234],\r\n \"odsInstanceId\": {{ODSInstanceId}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT Application Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"ApplicationName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"ClaimSetName\"].length).to.equal(1);\r", + " pm.expect(response.errors[\"EducationOrganizationIds\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"\",\r\n \"vendorId\": {{OtherApplicationVendorId}},\r\n \"claimSetName\": \"\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [],\r\n \"odsInstanceId\": {{ODSInstanceId}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Vendor", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application Invalid Vendor: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT Application Invalid Vendor: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid Vendor: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid Vendor: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.VendorId.length).to.equal(1);\r", + " pm.expect(response.errors.VendorId[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Updated Application Name\",\r\n \"vendorId\": 9999,\r\n \"claimSetName\": \"Ed-Fi ODS Admin App\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [1234],\r\n \"odsInstanceId\": 10\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Invalid Profile", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application Invalid Profile: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"PUT Application Invalid Profile: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid Profile: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application Invalid Profile: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ProfileIds.length).to.equal(1);\r", + " pm.expect(response.errors.ProfileIds[0]).to.contain(\"not found\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Updated Application Name\",\r\n \"vendorId\": {{OtherApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi ODS Admin App\",\r\n \"profileIds\": [9999],\r\n \"educationOrganizationIds\": [1234],\r\n \"odsInstanceId\": {{ODSInstanceId}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Reset Credential", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ResetCredentials: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"PUT ResetCredentials: Response result includes application key and secret\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + " pm.expect(result).to.have.property(\"key\");\r", + " pm.expect(result).to.have.property(\"secret\");\r", + "});\r", + "\r", + "const PutResetCredentialsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"key\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"secret\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"key\",\r", + " \"secret\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"PUT Reset Credentials: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PutResetCredentialsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}", + "reset-credential" + ] + } + }, + "response": [] + }, + { + "name": "Applications", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE Applications: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DELETE Applications: Response matches success format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE Applications: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"application\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "\r", + "const DeleteApplicationsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"DELETE Applications: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(DeleteApplicationsSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Application NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Application NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Application NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"application\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Reset Credential - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Reset Credential NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT Reset Credential NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT Reset Credential NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"application\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}", + "reset-credential" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Application NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"PUT Application NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT Application NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"application\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"applicationId\": {{CreatedApplicationId}},\r\n \"applicationName\": \"Test Application\",\r\n \"vendorId\": {{ApplicationVendorId}},\r\n \"claimSetName\": \"Ed-Fi Sandbox\",\r\n \"profileIds\": [],\r\n \"educationOrganizationIds\": [ 255901 ],\r\n \"odsInstanceId\": {{ODSInstanceId}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Applications - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL Application NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"DEL Application NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DEL Application NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"application\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedApplicationId\"));\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"ApplicationVendorId\");\r", + "pm.collectionVariables.unset(\"CreatedApplicationId\");\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "ClaimSets", + "item": [ + { + "name": "ClaimSets", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"ClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " pm.response.to.be.header(\"Location\", `/claimsets/${result.id}`);\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets: Response result includes claimSet key and secret\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + "});\r", + "\r", + "if(result.id) {\r", + " pm.collectionVariables.set(\"CreatedClaimSetId\", result.id);\r", + "}\r", + "\r", + "const PostClaimSetsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"resourceClaims\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"read\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"create\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"update\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"delete\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_defaultAuthStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"authStrategyId\",\r", + " \"authStrategyName\",\r", + " \"displayName\",\r", + " \"isInheritedFromParent\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"authStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"read\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"create\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"update\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"delete\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_defaultAuthStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"authStrategyId\",\r", + " \"authStrategyName\",\r", + " \"displayName\",\r", + " \"isInheritedFromParent\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"authStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"read\",\r", + " \"create\",\r", + " \"update\",\r", + " \"delete\",\r", + " \"_defaultAuthStrategiesForCRUD\",\r", + " \"authStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"read\",\r", + " \"create\",\r", + " \"update\",\r", + " \"delete\",\r", + " \"_defaultAuthStrategiesForCRUD\",\r", + " \"authStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"resourceClaims\",\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"POST ClaimSets: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PostClaimSetsSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Test ClaimSet {{ClaimSetGUID}}\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningStandard\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"\",\r\n \"resourceClaims\": []\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid JSON", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Json: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Json: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Json: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Json: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \r\n\t\"noname\": \"Not-Valid\",\r\n \"window\": {\r\n \"title\": \"Sample Konfabulator Widget\",\r\n \"name\": \"main_window\",\r\n \"width\": 500,\r\n \"height\": 500\r\n },\r\n \"image\": { \r\n \"src\": \"Images/Sun.png\",\r\n \"name\": \"sun1\",\r\n \"hOffset\": 250,\r\n \"vOffset\": 250,\r\n \"alignment\": \"center\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Existing ClaimSet Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OtherClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimsets`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"name\": `Other Test ClaimSet ${pm.collectionVariables.get(\"OtherClaimSetGUID\")}`,\r", + " \"resourceClaims\": []\r", + " }), \r", + " }\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSetJson = claimSetResponse.json();\r", + " if(!claimSetJson.id) { console.log('Error in Pre-request: claimset ID missing from response. Response is:', claimSetJson); }\r", + " pm.collectionVariables.set(\"OtherExistingClaimSetId\", claimSetJson.id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Name.length).to.equal(1);\r", + " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Other Test ClaimSet {{OtherClaimSetGUID}}\",\r\n \"resourceClaims\": []\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Wrong Resource Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Resource Name: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Resource Name: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Resource Name: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Resource Name: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(2);\r", + " [\"not in the system\", \"educationStandards-123\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " });\r", + " [\"not in the system\", \"learningStandard-123\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[1]).to.contain(substring);\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"ClaimSet-WithWrongResource\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards-123\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningStandard-123\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Wrong Parent Child Relationship", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Parent Child Relationship: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Parent Child Relationship: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Parent Child Relationship: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Parent Child Relationship: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(1);\r", + " [\"Child resource: 'academicSubjectDescriptor'\", \"wrong parent resource\", \"parent resource is: 'systemDescriptors'\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " });\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Wrong-Parent-Child-Relation\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Resource Duplication", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Resource Duplcation: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Resource Duplcation: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Resource Duplcation: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Resource Duplcation: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(1);\r", + " [\"Only unique resource claims\", \"duplicate resource: 'learningStandard'\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Resource-Duplication\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningStandard\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n },\r\n\t\t {\r\n \"name\": \"learningStandard\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Add Action", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResourceClaimAction: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4,\r\n \"resourceClaimActions\": {\r\n \"create\": true,\r\n \"read\": true,\r\n \"update\": true,\r\n \"delete\": false\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaimActions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaimActions" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Add Action Validation Errors", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResourceClaimAction Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ResourceClaimAction Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ResourceClaimAction Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ResourceClaimAction Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(2);\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(\"which is not in the system\");\r", + " pm.expect(response.errors.ResourceClaims[1]).to.contain(\"have at least one action\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4000,\r\n \"resourceClaimActions\": {\r\n \"create\": false,\r\n \"read\": false,\r\n \"update\": false,\r\n \"delete\": false\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaimActions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaimActions" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Modify Action", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ResourceClaimAction: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4,\r\n \"resourceClaimActions\": {\r\n \"create\": true,\r\n \"read\": true,\r\n \"update\": false,\r\n \"delete\": false\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaimActions/4", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaimActions", + "4" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Modify Action ClaimSet not found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ResourceClaimAction Not Found: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ResourceClaimAction Not Found: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT ResourceClaimAction Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"claimset\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceClaimId\": 4,\r\n \"resourceClaimActions\": {\r\n \"create\": true,\r\n \"read\": true,\r\n \"update\": false,\r\n \"delete\": false\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/90000/resourceclaimActions/4", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "90000", + "resourceclaimActions", + "4" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Override Auth Strategy", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OverrideAuthStrategy: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"actionName\": \"create\",\r\n \"authStrategyName\": \"RelationshipsWithStudentsOnly\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaims/4/overrideauthstrategy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaims", + "4", + "overrideauthstrategy" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Override Auth Strategy Validation Errors", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST OverrideAuthStrategy Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST OverrideAuthStrategy Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.ResourceClaim.length).to.equal(1);\r", + " pm.expect(response.errors.ResourceClaim[0]).to.contain(\"doesn't exist\");\r", + " pm.expect(response.errors.AuthStrategyName.length).to.equal(1);\r", + " pm.expect(response.errors.AuthStrategyName[0]).to.contain(\"doesn't exist\");\r", + " pm.expect(response.errors.ActionName.length).to.equal(1);\r", + " pm.expect(response.errors.ActionName[0]).to.contain(\"doesn't exist\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"actionName\": \"NotExist\",\r\n \"authStrategyName\": \"RelationshipsWithStudentsOnlys\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaims/4000/overrideauthstrategy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaims", + "4000", + "overrideauthstrategy" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Reset Auth Strategies", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResetAuthStrategies: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaims/4/resetauthstrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaims", + "4", + "resetauthstrategies" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Reset Auth Strategies Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ResetAuthStrategies: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaims/40000/resetauthstrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaims", + "40000", + "resetauthstrategies" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Delete", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE ResourceClaimOnClaimSet: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaims/4", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaims", + "4" + ] + } + }, + "response": [] + }, + { + "name": "ResourceClaimClaimSets - Delete Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE ResourceClaimOnClaimSet: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}/resourceclaims/4000", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}", + "resourceclaims", + "4000" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets/Copy- Invalid Existing ClaimSet Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OtherClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimsets`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"name\": `Other Test ClaimSet ${pm.collectionVariables.get(\"OtherClaimSetGUID\")}`,\r", + " \"resourceClaims\": []\r", + " }), \r", + " }\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSetJson = claimSetResponse.json();\r", + " if(!claimSetJson.id) { console.log('Error in Pre-request: claimset ID missing from response. Response is:', claimSetJson); }\r", + " pm.collectionVariables.set(\"OtherExistingClaimSetId\", claimSetJson.id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets Invalid Existing ClaimSets: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Name.length).to.equal(1);\r", + " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Other Test ClaimSet {{OtherClaimSetGUID}}\",\r\n \"resourceClaims\": []\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets/Copy", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"OtherClaimSetGUID\", pm.variables.replaceIn('{{$guid}}'));\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimsets`,\r", + " method: 'POST',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"name\": `Other Test ClaimSet ${pm.collectionVariables.get(\"OtherClaimSetGUID\")}`,\r", + " \"resourceClaims\": [{\r", + " \"name\": \"educationStandards\",\r", + " \"read\": true,\r", + " \"create\": true,\r", + " \"update\": true,\r", + " \"delete\": true,\r", + " \"defaultAuthStrategiesForCRUD\": [\r", + " {\r", + " \"authStrategyName\": \"NamespaceBased\",\r", + " \"isInheritedFromParent\": false\r", + " },\r", + " {\r", + " \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r", + " \"isInheritedFromParent\": false\r", + " },\r", + " {\r", + " \"authStrategyName\": \"NamespaceBased\",\r", + " \"isInheritedFromParent\": false\r", + " },\r", + " {\r", + " \"authStrategyName\": \"NamespaceBased\",\r", + " \"isInheritedFromParent\": false\r", + " }\r", + " ],\r", + " \"authStrategyOverridesForCRUD\": [\r", + " null,\r", + " null,\r", + " null,\r", + " null\r", + " ]\r", + " }]\r", + " })\r", + " }\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSetJson = claimSetResponse.json();\r", + " if(!claimSetJson.id) { console.log('Error in Pre-request: claimset ID missing from response. Response is:', claimSetJson); }\r", + " pm.collectionVariables.set(\"OtherExistingClaimSetId\", claimSetJson.id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSets: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"POST ClaimSets: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " pm.response.to.be.header(\"Location\", `/claimsets/${result.id}`);\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSets: Response result claimset has expected name and resource claims\", function () {\r", + " pm.expect(result).to.have.property(\"id\");\r", + " pm.expect(result).to.have.property(\"name\");\r", + " pm.expect(result.name).contains(\"Copied ClaimSet from\");\r", + " pm.expect(result.resourceClaims).to.not.be.empty;\r", + " const resourceclaimexists = result.resourceClaims.any(r => r.name === \"educationStandards\")\r", + " pm.expect(resourceclaimexists).to.equal(true);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Copied ClaimSet from {{OtherExistingClaimSetId}}\",\r\n \"originalid\": {{OtherExistingClaimSetId}}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/copy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "copy" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets/Copy- Invalid ClaimSet Id", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "function randomIntFromInterval(min, max) { // min and max included \r", + " return Math.floor(Math.random() * (max - min + 1) + min)\r", + "}\r", + "\r", + "const rndInt = randomIntFromInterval(450, 783)\r", + "pm.collectionVariables.set(\"NotExistClaimSetId\", pm.variables.replaceIn(rndInt));\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST ClaimSet NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSet NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"POST ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + " pm.expect(response.title).to.contains(\"Not found\");\r", + " pm.expect(response.title).to.contains(\"claimset\"); \r", + "});\r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Claim set does not exists\",\r\n \"originalid\": {{NotExistClaimSetId}}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/copy", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "copy" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const results = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSets: Response result includes claimsets\", function () {\r", + " pm.expect(results.length).to.be.greaterThan(0);\r", + "\r", + " const indexOfClaimSet = results.map(\r", + " function(claimSet) { return claimSet.id; }\r", + " ).indexOf(pm.collectionVariables.get(\"CreatedClaimSetId\"));\r", + "\r", + " const result = results[indexOfClaimSet];\r", + " pm.expect(result.name).to.equal(`Test ClaimSet ${pm.collectionVariables.get(\"ClaimSetGUID\")}`);\r", + " pm.expect(result._isSystemReserved).to.equal(false);\r", + " pm.expect(result._applications).to.be.empty;\r", + "});\r", + "\r", + "const GetClaimSetsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSet: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSetsId: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"GET ClaimSetsId: Response result matches claimset\", function () {\r", + " const claimSetId = pm.collectionVariables.get(\"CreatedClaimSetId\");\r", + " \r", + " pm.expect(result.id).to.equal(claimSetId);\r", + " pm.expect(result.name).to.equal(`Test ClaimSet ${pm.collectionVariables.get(\"ClaimSetGUID\")}`);\r", + " pm.expect(result._isSystemReserved).to.equal(false);\r", + " pm.expect(result._applications).to.be.empty;\r", + " pm.expect(result.resourceClaims).to.not.be.empty;\r", + " const educationStandardsResourceClaim = result.resourceClaims.find(r => r.name === \"educationStandards\")\r", + " pm.expect(educationStandardsResourceClaim).to.be.an(\"object\", \"The educationStandards resource claim was not found.\")\r", + " const academicSubjectDescriptorResourceClaim = result.resourceClaims.find(r => r.name === \"academicSubjectDescriptor\")\r", + " pm.expect(academicSubjectDescriptorResourceClaim).to.be.an(\"object\", \"The academicSubjectDescriptor resource claim was not found.\")\r", + "});\r", + "\r", + "const GetClaimSetsIdSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"resourceClaims\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"read\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"create\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"update\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"delete\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_defaultAuthStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"authStrategyId\",\r", + " \"authStrategyName\",\r", + " \"displayName\",\r", + " \"isInheritedFromParent\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"authStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"read\",\r", + " \"create\",\r", + " \"update\",\r", + " \"delete\",\r", + " \"_defaultAuthStrategiesForCRUD\",\r", + " \"authStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"resourceClaims\",\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET ClaimSetId: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetClaimSetsIdSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const result = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets: Response result includes updated claimset\", function () {\r", + " pm.expect(result.name).to.equal(\"Updated Test ClaimSet\");\r", + " pm.expect(result._isSystemReserved).to.equal(false);\r", + " pm.expect(result._applications).to.be.empty;\r", + " pm.expect(result.resourceClaims).to.not.be.empty;\r", + " const educationStandardsResourceClaim = result.resourceClaims.find(r => r.name === \"educationStandards\")\r", + " pm.expect(educationStandardsResourceClaim).to.be.an(\"object\", \"The educationStandards resource claim was not found.\")\r", + " const academicSubjectDescriptorResourceClaim = result.resourceClaims.any(r => r.name === \"academicSubjectDescriptor\")\r", + " pm.expect(academicSubjectDescriptorResourceClaim).to.equal(false);\r", + "});\r", + "\r", + "const PutClaimSetsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"resourceClaims\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"read\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"create\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"update\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"delete\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_defaultAuthStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"authStrategyId\",\r", + " \"authStrategyName\",\r", + " \"displayName\",\r", + " \"isInheritedFromParent\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"authStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"read\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"create\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"update\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"delete\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_defaultAuthStrategiesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"isInheritedFromParent\": {\r", + " \"type\": \"boolean\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"authStrategyId\",\r", + " \"authStrategyName\",\r", + " \"displayName\",\r", + " \"isInheritedFromParent\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"authStrategyOverridesForCRUD\": {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " },\r", + " {\r", + " \"type\": \"null\"\r", + " }\r", + " ]\r", + " },\r", + " \"children\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"read\",\r", + " \"create\",\r", + " \"update\",\r", + " \"delete\",\r", + " \"_defaultAuthStrategiesForCRUD\",\r", + " \"authStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"name\",\r", + " \"read\",\r", + " \"create\",\r", + " \"update\",\r", + " \"delete\",\r", + " \"_defaultAuthStrategiesForCRUD\",\r", + " \"authStrategyOverridesForCRUD\",\r", + " \"children\"\r", + " ]\r", + " }\r", + " ]\r", + " },\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"_isSystemReserved\": {\r", + " \"type\": \"boolean\"\r", + " },\r", + " \"_applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"resourceClaims\",\r", + " \"id\",\r", + " \"name\",\r", + " \"_isSystemReserved\",\r", + " \"_applications\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"PUT ClaimSets: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(PutClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"Updated Test ClaimSet\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningStandard\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n } \r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"\",\r\n \"resourceClaims\": []\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid JSON", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid Json: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Json: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Json: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Json: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}}, \r\n\t\"noname\": \"Not-Valid\",\r\n \"window\": {\r\n \"title\": \"Sample Konfabulator Widget\",\r\n \"name\": \"main_window\",\r\n \"width\": 500,\r\n \"height\": 500\r\n },\r\n \"image\": { \r\n \"src\": \"Images/Sun.png\",\r\n \"name\": \"sun1\",\r\n \"hOffset\": 250,\r\n \"vOffset\": 250,\r\n \"alignment\": \"center\"\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Existing ClaimSet Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid Existing ClaimSets: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Existing ClaimSets: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Existing ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Existing ClaimSets: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors.Name.length).to.equal(1);\r", + " pm.expect(response.errors.Name[0]).to.contain(\"already exists\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"Other Test ClaimSet {{OtherClaimSetGUID}}\",\r\n \"resourceClaims\": []\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Wrong Resource Name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid Wrong Resource: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Wrong Resource: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Wrong Resource: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Wrong Resource: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(2);\r", + " [\"not in the system\", \"educationStandards-123\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " });\r", + " [\"not in the system\", \"learningStandard-123\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[1]).to.contain(substring);\r", + " });\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"ClaimSet-WithWrongResource\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards-123\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningStandard-123\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Wrong Parent Child Relationship", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid Wrong Parent Child: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Wrong Parent Child: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Wrong Parent Child: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Wrong Parent Child: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(1);\r", + " [\"Child resource: 'academicSubjectDescriptor'\", \"wrong parent resource\", \"parent resource is: 'systemDescriptors'\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " });\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"Wrong-Parent-Child-Relation\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Invalid Resource Duplication", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets Invalid Resource Duplication: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "const errors = pm.response.json().errors;\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Resource Duplication: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Resource Duplication: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets Invalid Resource Duplication: Response errors include messages by property and resource\", function () {\r", + " pm.expect(response.errors.ResourceClaims.length).to.equal(1);\r", + " [\"Only unique resource claims\", \"duplicate resource: 'learningStandard'\"].forEach((substring) => {\r", + " pm.expect(response.errors.ResourceClaims[0]).to.contain(substring);\r", + " });\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"Resource-Duplication\",\r\n \"resourceClaims\": [\r\n {\r\n \"name\": \"educationStandards\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": false\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": [\r\n {\r\n \"name\": \"learningStandard\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n },\r\n\t\t {\r\n \"name\": \"learningStandard\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n }\r\n ]\r\n },\r\n {\r\n \"name\": \"academicSubjectDescriptor\",\r\n \"read\": true,\r\n \"create\": true,\r\n \"update\": true,\r\n \"delete\": true,\r\n \"defaultAuthStrategiesForCRUD\": [\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NoFurtherAuthorizationRequired\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n },\r\n {\r\n \"authStrategyName\": \"NamespaceBased\",\r\n \"isInheritedFromParent\": true\r\n }\r\n ],\r\n \"authStrategyOverridesForCRUD\": [\r\n null,\r\n null,\r\n null,\r\n null\r\n ],\r\n \"children\": []\r\n } \r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - System Reserved", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/claimsets?offset=${pm.variables.get(\"offset\")}&limit=${pm.variables.get(\"limit\")}`,\r", + " method: 'GET',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " }\r", + "},\r", + "function (claimSetErr, claimSetResponse) {\r", + " if(claimSetErr) { console.log(\"Error in Pre-request:\", claimSetErr); }\r", + " const claimSets = claimSetResponse.json();\r", + " if(!claimSets) { console.log('Error in Pre-request: ClaimSets missing from response.'); }\r", + " const systemReservedClaimSetIds = claimSets.map(\r", + " function(claimSet) { \r", + " if(claimSet._isSystemReserved)\r", + " {\r", + " return claimSet.id;\r", + " } \r", + " }\r", + " );\r", + " if(!systemReservedClaimSetIds) { console.log('Error in Pre-request: System Reserved claimset IDs not found. Response is:', claimSets); }\r", + " pm.collectionVariables.set(\"SystemReservedClaimSetId\", systemReservedClaimSetIds[0]);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSets System Reserved: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSets System Reserved: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets System Reserved: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSets System Reserved: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"id\"].length).to.equal(1);\r", + " [\"AB Connect\", \"system reserved\"].forEach((substring) => {\r", + " pm.expect(response.errors.id[0]).to.contain(substring);\r", + " });\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{SystemReservedClaimSetId}},\r\n \"name\": \"Update System Reserved ClaimSet\",\r\n \"resourceClaims\": []\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{SystemReservedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{SystemReservedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE ClaimSets: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DELETE ClaimSets: Response matches success format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DELETE ClaimSets: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"claimset\");\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "\r", + "const DeleteClaimSetsSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"title\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"title\"\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"DELETE ClaimSet: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(DeleteClaimSetsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - System Reserved", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL ClaimSets System Reserved: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DEL ClaimSets System Reserved: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSets System Reserved: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSets System Reserved: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"id\"].length).to.equal(1);\r", + " [\"AB Connect\", \"system reserved\"].forEach((substring) => {\r", + " pm.expect(response.errors.id[0]).to.contain(substring);\r", + " });\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"SystemReservedClaimSetId\");\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{SystemReservedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{SystemReservedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - With Applications", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL ClaimSets With Application: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"DEL ClaimSets With Application: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSets With Application: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSets With Application: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"id\"].length).to.equal(1);\r", + " [\"Cannot delete\", \"associated application\"].forEach((substring) => {\r", + " pm.expect(response.errors.id[0]).to.contain(substring);\r", + " });\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"OtherApplicationId\");\r", + "pm.collectionVariables.unset(\"OtherApplicationVendorId\");\r", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + " pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/applications/${pm.collectionVariables.get(\"OtherApplicationId\")}`,\r", + " method: 'PUT',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " },\r", + " body: {\r", + " mode: 'raw',\r", + " raw:JSON.stringify({\r", + " \"applicationName\": \"ClaimSet Test Vendor Application\",\r", + " \"vendorId\": pm.collectionVariables.get(\"OtherApplicationVendorId\"),\r", + " \"claimSetName\": `Other Test ClaimSet ${pm.collectionVariables.get(\"OtherClaimSetGUID\")}`,\r", + " \"profileIds\": [],\r", + " \"educationOrganizationIds\": [ 255901 ],\r", + " \"odsInstanceId\": 1\r", + " }),\r", + " }\r", + " }, \r", + " function (appErr, appResonse) {\r", + " if(appErr) { console.log(\"Error in Pre-request:\", appErr); }\r", + " const appJson = appResonse.json();\r", + " if(!appJson.id) { console.log('Error in Pre-request: applicationId missing from response. Response is:', appJson); }\r", + "}); " + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{OtherExistingClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{OtherExistingClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ClaimSet NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET ClaimSet NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"claimset\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedClaimSetId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT ClaimSet NotFound: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"PUT ClaimSet NotFound: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"PUT ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"deleted\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": {{CreatedClaimSetId}},\r\n \"name\": \"Edited-ClaimSet\",\r\n \"resourceClaims\": []\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + }, + { + "name": "ClaimSets - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DEL ClaimSet NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSet NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"DEL ClaimSet NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"claimset\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"CreatedClaimSetId\"));\r", + "});\r", + "\r", + "pm.collectionVariables.unset(\"CreatedClaimSetId\");\r", + "pm.collectionVariables.unset(\"OtherExistingClaimSetId\");\r", + "pm.collectionVariables.unset(\"ClaimSetGUID\");\r", + "pm.collectionVariables.unset(\"OtherClaimSetGUID\");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Resourceclaims", + "item": [ + { + "name": "Resourceclaims", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaims: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET ResourceClaims: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"parentId\");", + " pm.expect(response[0]).to.have.property(\"parentName\");", + " pm.expect(response[0]).to.have.property(\"children\");", + "});", + "", + "const GetResourceClaimsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"null\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " },", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaims: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsSchema);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceclaims", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceclaims" + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimsId: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET ResourceClaimsId: Response matches success format\", function () {", + " pm.expect(response).to.have.property(\"id\");", + " pm.expect(response).to.have.property(\"name\");", + " pm.expect(response).to.have.property(\"parentId\");", + " pm.expect(response).to.have.property(\"parentName\");", + " pm.expect(response).to.have.property(\"children\");", + "});", + "", + "const GetResourceClaimsIdSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"null\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"parentId\": {", + " \"type\": \"integer\"", + " },", + " \"parentName\": {", + " \"type\": \"string\"", + " },", + " \"children\": {", + " \"type\": \"array\",", + " \"items\": {}", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + " }", + " ]", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"name\",", + " \"parentId\",", + " \"parentName\",", + " \"children\"", + " ]", + "}", + "", + "pm.test(\"GET ResourceClaimsId: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetResourceClaimsIdSchema);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceclaims/1", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceclaims", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims by ID Children", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ResourceClaimsId Children: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET ResourceClaimsId Children: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"parentId\");", + " pm.expect(response[0]).to.have.property(\"parentName\");", + " pm.expect(response[0]).to.have.property(\"children\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceclaims/1/children", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceclaims", + "1", + "children" + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims Children - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Resourceclaim by Id Children NotFound: Status code is Not Found\", function () {", + " pm.response.to.have.status(404);", + "});", + "", + "pm.test(\"GET Resourceclaim by Id Children NotFound: Response matches error format\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response).to.have.property(\"title\");", + "});", + "", + "pm.test(\"GET Resourceclaim by Id Children NotFound: Response title is helpful and accurate\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response.title).to.contain(\"Not found\");", + " pm.expect(response.title).to.contain(\"resourceclaim\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceclaims/000/children", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceclaims", + "000", + "children" + ] + } + }, + "response": [] + }, + { + "name": "Resourceclaims -Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Resourceclaim NotFound: Status code is Not Found\", function () {", + " pm.response.to.have.status(404);", + "});", + "", + "pm.test(\"GET Resourceclaim NotFound: Response matches error format\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response).to.have.property(\"title\");", + "});", + "", + "pm.test(\"GET Resourceclaim NotFound: Response title is helpful and accurate\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response.title).to.contain(\"Not found\");", + " pm.expect(response.title).to.contain(\"resourceclaim\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/resourceclaims/00", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "resourceclaims", + "00" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "AuthStrategies", + "item": [ + { + "name": "Authstrategies", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET AuthStrategies: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"GET AuthStrategies: Response matches success format\", function () {\r", + " pm.expect(response[0]).to.have.property(\"authStrategyId\");\r", + " pm.expect(response[0]).to.have.property(\"authStrategyName\");\r", + " pm.expect(response[0]).to.have.property(\"displayName\");\r", + "});\r", + "\r", + "const GetAuthStrategiesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"authStrategyId\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"authStrategyName\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"displayName\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"authStrategyId\",\r", + " \"authStrategyName\",\r", + " \"displayName\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET AuthStrategies: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetAuthStrategiesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/authstrategies", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "authstrategies" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Profiles", + "item": [ + { + "name": "Profiles", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles: Status code is Created\", function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles: Response includes location in header\", function () {\r", + " pm.response.to.have.header(\"Location\");\r", + " const id = pm.response.headers.get(\"Location\").split(\"/\")[2];\r", + " if(id)\r", + " {\r", + " pm.collectionVariables.set(\"CreatedProfileId\", id);\r", + " }\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{API_URL}}/v2/Profiles/" + }, + "response": [] + }, + { + "name": "Profiles - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Name\"][0].toLowerCase()).to.contain(\"'name' must not be empty\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{API_URL}}/v2/Profiles/" + }, + "response": [] + }, + { + "name": "Profiles - Invalid definition xml", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Definition\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"test\");\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"readcontenttype\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{API_URL}}/v2/Profiles/" + }, + "response": [] + }, + { + "name": "Profiles - Name mismatch", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Definition\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"match with test-profile-123\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile-123\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{API_URL}}/v2/Profiles/" + }, + "response": [] + }, + { + "name": "Profiles", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profiles: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET Profiles: Response result includes profiles\", function () {\r", + " const results = pm.response.json();\r", + " pm.expect(results.length).to.be.greaterThan(0); \r", + "});\r", + "\r", + "const GetProfilesSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\" \r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Profile: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetProfilesSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/Profiles/?{{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "Profiles", + "" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Profile by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET ProfileById: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"GET ProfileById: Response result matches profile\", function () {\r", + " const result = pm.response.json();\r", + " pm.expect(result.name).to.equal(`Test-Profile`);\r", + " pm.expect(result.definition).to.not.be.empty; \r", + "});\r", + "\r", + "const GetProfileIdSchema = {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"definition\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"applications\": {\r", + " \"type\": \"array\",\r", + " \"items\": {}\r", + " }\r", + " }\r", + "}\r", + "\r", + "pm.test(\"GET ProfileById: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetProfileIdSchema);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": "{{API_URL}}/v2/profiles/{{CreatedProfileId}}" + }, + "response": [] + }, + { + "name": "Profile by ID - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profile NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"profile\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistProfileId\"));\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": "{{API_URL}}/v2/profiles/{{NotExistProfileId}}" + }, + "response": [] + }, + { + "name": "Profiles", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"PUT Profiles: Status code is Created\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.sendRequest({\r", + " url: `${pm.variables.get(\"API_URL\")}/v2/profiles/${pm.collectionVariables.get(\"CreatedProfileId\")}`,\r", + " method: 'GET',\r", + " header: {\r", + " \"Content-Type\": \"application/json\",\r", + " \"Authorization\": `Bearer ${pm.collectionVariables.get(\"TOKEN\")}`\r", + " }\r", + "},\r", + "function (profileErr, profileResponse) {\r", + " if(profileErr) \r", + " { \r", + " console.log(\"Error :\", profileErr); \r", + " } \r", + " const updatedProfileJson = profileResponse.json();\r", + " pm.test(\"PUT Profiles: Response includes updated profile\", function () {\r", + " pm.expect(updatedProfileJson.name).to.equal(\"Updated-Test-Profile\");\r", + " pm.expect(updatedProfileJson.definition).to.not.be.empty;\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Updated-Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{API_URL}}/v2/Profiles/{{CreatedProfileId}}" + }, + "response": [] + }, + { + "name": "Profiles - Not Found", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profile NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"profile\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistProfileId\"));\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Not-Found\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{API_URL}}/v2/profiles/{{NotExistProfileId}}" + }, + "response": [] + }, + { + "name": "Profiles - Invalid", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Name\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Name\"][0].toLowerCase()).to.contain(\"'name' must not be empty\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{API_URL}}/v2/Profiles/{{CreatedProfileId}}" + }, + "response": [] + }, + { + "name": "Profiles - Invalid definition xml", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Definition\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"test\");\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"readcontenttype\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Updated-Test-Profile\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{API_URL}}/v2/Profiles/{{CreatedProfileId}}" + }, + "response": [] + }, + { + "name": "Profiles - Name mismatch", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"POST Profiles Invalid: Status code is Bad Request\", function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response matches error format\", function () {\r", + " pm.expect(response).to.have.property(\"title\");\r", + " pm.expect(response).to.have.property(\"errors\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response title is helpful and accurate\", function () {\r", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages by property\", function () {\r", + " pm.expect(response.errors[\"Definition\"].length).to.equal(1);\r", + "});\r", + "\r", + "pm.test(\"POST Profiles Invalid: Response errors include messages with wrong element name\", function () {\r", + " pm.expect(response.errors[\"Definition\"][0].toLowerCase()).to.contain(\"match with test-profile-123\");\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Test-Profile-123\",\r\n \"Definition\": \"\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{API_URL}}/v2/Profiles/{{CreatedProfileId}}" + }, + "response": [] + }, + { + "name": "Profiles", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"DELETE Profile: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": "{{API_URL}}/v2/profiles/{{CreatedProfileId}}" + }, + "response": [] + }, + { + "name": "Profiles - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Profile NotFound: Status code is Not Found\", function () {\r", + " pm.response.to.have.status(404);\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response matches error format\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response).to.have.property(\"title\");\r", + "});\r", + "\r", + "pm.test(\"GET Profile NotFound: Response title is helpful and accurate\", function () {\r", + " const response = pm.response.json();\r", + "\r", + " pm.expect(response.title).to.contain(\"Not found\");\r", + " pm.expect(response.title).to.contain(\"profile\");\r", + " pm.expect(response.title).to.contain(pm.collectionVariables.get(\"NotExistProfileId\"));\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": "{{API_URL}}/v2/profiles/{{NotExistProfileId}}" + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "function randomIntFromInterval(min, max) { // min and max included ", + " return Math.floor(Math.random() * (max - min + 1) + min)", + "}", + "", + "const rndInt = randomIntFromInterval(450, 783)", + "pm.collectionVariables.set(\"NotExistProfileId\", pm.variables.replaceIn(rndInt));" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "Actions", + "item": [ + { + "name": "Authstrategies", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Actions: Status code is OK\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "const response = pm.response.json();\r", + "\r", + "pm.test(\"GET Actions: Response matches success format\", function () {\r", + " pm.expect(response[0]).to.have.property(\"id\");\r", + " pm.expect(response[0]).to.have.property(\"name\");\r", + " pm.expect(response[0]).to.have.property(\"uri\");\r", + "});\r", + "\r", + "const GetActionsSchema = {\r", + " \"type\": \"array\",\r", + " \"items\": [\r", + " {\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + " \"id\": {\r", + " \"type\": \"integer\"\r", + " },\r", + " \"name\": {\r", + " \"type\": \"string\"\r", + " },\r", + " \"uri\": {\r", + " \"type\": \"string\"\r", + " }\r", + " },\r", + " \"required\": [\r", + " \"id\",\r", + " \"name\",\r", + " \"uri\"\r", + " ]\r", + " }\r", + " ]\r", + "}\r", + "\r", + "pm.test(\"GET Actions: Validation Schema Response\", () => {\r", + " pm.response.to.have.jsonSchema(GetActionsSchema);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/actions", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "actions" + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "v1", + "item": [ + { + "name": "OdsInstances", + "item": [ + { + "name": "OdsInstances", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstances: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET OdsInstances: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"odsInstanceId\");", + " pm.expect(response[0]).to.have.property(\"name\");", + " pm.expect(response[0]).to.have.property(\"instanceType\");", + "});", + "", + "const GetOdsInstancesSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"odsInstanceId\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"instanceType\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"odsInstanceId\",", + " \"name\",", + " \"instanceType\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET OdsInstances: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesSchema);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/odsinstances?offset={{offset}}&limit={{limit}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsinstances" + ], + "query": [ + { + "key": "offset", + "value": "{{offset}}" + }, + { + "key": "limit", + "value": "{{limit}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances - Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstances NotFound: Status code is Not Found\", function () {", + " pm.response.to.have.status(404);", + "});", + "", + "pm.test(\"GET OdsInstances NotFound: Response matches error format\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response).to.have.property(\"title\");", + "});", + "", + "pm.test(\"GET OdsInstances NotFound: Response title is helpful and accurate\", function () {", + " const response = pm.response.json();", + "", + " pm.expect(response.title).to.contain(\"Not found\");", + " pm.expect(response.title).to.contain(\"odsinstance\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/odsinstances/0", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsinstances", + "0" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstancesID: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET OdsInstancesID: Response matches success format\", function () {", + " pm.expect(response).to.have.property(\"odsInstanceId\");", + " pm.expect(response).to.have.property(\"name\");", + " pm.expect(response).to.have.property(\"instanceType\");", + "});", + "", + "const GetOdsInstancesIdSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"odsInstanceId\": {", + " \"type\": \"integer\"", + " },", + " \"name\": {", + " \"type\": \"string\"", + " },", + " \"instanceType\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"odsInstanceId\",", + " \"name\",", + " \"instanceType\"", + " ]", + "}", + "", + "pm.test(\"GET OdsInstancesID: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesIdSchema);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/odsinstances/{{ODSInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsinstances", + "{{ODSInstanceId}}" + ] + } + }, + "response": [] + }, + { + "name": "OdsInstances by ID Application", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET OdsInstancesIDApplication: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"GET OdsInstancesIDApplication: Response matches success format\", function () {", + " pm.expect(response[0]).to.have.property(\"id\");", + " pm.expect(response[0]).to.have.property(\"applicationName\");", + " pm.expect(response[0]).to.have.property(\"claimSetName\");", + " pm.expect(response[0]).to.have.property(\"educationOrganizationIds\");", + " pm.expect(response[0]).to.have.property(\"vendorId\");", + " pm.expect(response[0]).to.have.property(\"profileIds\");", + " pm.expect(response[0]).to.have.property(\"odsInstanceId\");", + "});", + "", + "const GetOdsInstancesIdApplication = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": {", + " \"type\": \"integer\"", + " },", + " \"applicationName\": {", + " \"type\": \"string\"", + " },", + " \"claimSetName\": {", + " \"type\": \"string\"", + " },", + " \"educationOrganizationIds\": {", + " \"type\": \"array\",", + " \"items\": {}", + " },", + " \"odsInstanceName\": {", + " \"type\": \"string\"", + " },", + " \"vendorId\": {", + " \"type\": \"integer\"", + " },", + " \"profileIds\": {", + " \"type\": \"array\",", + " \"items\": {}", + " },", + " \"odsInstanceId\": {", + " \"type\": \"integer\"", + " }", + " },", + " \"required\": [", + " \"id\",", + " \"applicationName\",", + " \"claimSetName\",", + " \"educationOrganizationIds\",", + " \"vendorId\",", + " \"profileIds\",", + " \"odsInstanceId\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET OdsInstancesIdApplication: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetOdsInstancesIdApplication);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/odsinstances/{{ODSInstanceId}}/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsinstances", + "{{ODSInstanceId}}", + "applications" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if(pm.request.auth && pm.request.auth.type === \"noauth\") {", + " return;", + "}", + "", + "let currentToken = pm.collectionVariables.get(\"TOKEN\");", + "if(currentToken) {", + " return;", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + " },", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: pm.variables.get(\"RegisteredClientId\")},", + " {key: 'client_secret', value: pm.variables.get(\"RegisteredClientSecret\")},", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + }, + { + "key": "ODSInstanceId", + "value": "" + }, + { + "key": "NotExistClaimSetId", + "value": "" + }, + { + "key": "OtherApplicationVendorId", + "value": "" + }, + { + "key": "OtherApplicationId", + "value": "" + }, + { + "key": "ClaimSetGUID", + "value": "" + }, + { + "key": "CreatedClaimSetId", + "value": "" + }, + { + "key": "OtherClaimSetGUID", + "value": "" + }, + { + "key": "OtherExistingClaimSetId", + "value": "" + }, + { + "key": "RegisteredClientId", + "value": "" + }, + { + "key": "RegisteredClientSecret", + "value": "" + }, + { + "key": "ApplicationVendorId", + "value": "" + }, + { + "key": "CreatedApplicationId", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API.postman_environment.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API.postman_environment.json new file mode 100644 index 000000000..151f9246c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API.postman_environment.json @@ -0,0 +1,57 @@ +{ + "id": "dde8ae58-e386-4794-a055-ac62bc2e2eaa", + "name": "Admin API", + "values": [ + { + "key": "API_URL", + "value": "https://localhost:7214", + "type": "default", + "enabled": true + }, + { + "key": "offset", + "value": "0", + "type": "default", + "enabled": true + }, + { + "key": "limit", + "value": "1000", + "type": "default", + "enabled": true + }, + { + "key": "connectionString", + "value": "host=localhost;port=5401;username=postgres;password=P@ssw0rd;database=EdFi_Admin;pooling=false", + "type": "default", + "enabled": true + }, + { + "key": "securityconnectionString", + "value": "host=localhost;port=5401;username=postgres;password=P@ssw0rd;database=EdFi_Security;pooling=false", + "type": "default", + "enabled": true + }, + { + "key": "isMultitenant", + "value": "false", + "type": "default", + "enabled": true + }, + { + "key": "sortByProperty", + "value": "function sortByProperty(objArray, prop, direction){ if (arguments.length<2) throw new Error(\"ARRAY, AND OBJECT PROPERTY MINIMUM ARGUMENTS, OPTIONAL DIRECTION\"); if (!Array.isArray(objArray)) throw new Error(\"FIRST ARGUMENT NOT AN ARRAY\"); const clone = objArray.slice(0); const direct = arguments.length>2 ? arguments[2] : 1; const propPath = (prop.constructor===Array) ? prop : prop.split(\".\"); clone.sort(function(a,b){ for (let p in propPath){ if (a[propPath[p]] && b[propPath[p]]){ a = a[propPath[p]]; b = b[propPath[p]]; } } a = a.match(/^\\d+$/) ? +a : a; b = b.match(/^\\d+$/) ? +b : b; return ( (a < b) ? -1*direct : ((a > b) ? 1*direct : 0) ); }); return clone; }", + "type": "default", + "enabled": true + }, + { + "key": "jsHelper", + "value": "(function () { const rndChar = function (str) { return str.charAt(Math.floor(Math.random() * str.length)); }; const shfChar = function (str) { const array = str.split(''); for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array.join(''); }; return { generateClientSecret: function(){ const minLength = 32; const maxLength = 128; let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~'; const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength; result += rndChar('abcdefghijklmnopqrstuvwxyz'); result += rndChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ'); result += rndChar('0123456789'); result += rndChar(specialCharacters); for (let i = result.length; i < length; i++) { const charactersPlusSpecial = characters + specialCharacters; result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length)); } return shfChar(result); }, randomChar : rndChar, shuffleString: shfChar } })", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2024-08-26T03:23:09.412Z", + "_postman_exported_using": "Postman/11.9.2" +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/README.md b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/README.md new file mode 100644 index 000000000..93a07cbb3 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/README.md @@ -0,0 +1,69 @@ +# Admin API End to End Tests + +The collection **Admin API E2E** contains a set of tests to verify the +functionality of Admin Api. + +The **Admin API** environment file contains the variables needed to execute the +tests. + +## Run with Postman + +Import the collection and the environment into +[Postman](https://learning.postman.com/docs/getting-started/importing-and-exporting-data/). + +Change the value of the environment variables to the desired values, especially +the *API_URL* variable to match your Admin API URL. + +In the API calls, there are examples of with different results for guidance. + +To execute the API calls, you can either modify the request to set a specific +value. Additionally, you can execute the API calls in the order where a previous +API call sets the required variables. + +Example: + +```mermaid +flowchart LR; + + A[POST /vendor]-->|Generates| B(VendorId) + B --> D[PUT /vendor/id] + B --> E[DELETE /vendor/id] +``` + +In this example, adding a Vendor will save the VendorId into the variables, then +the update and delete methods will use the saved values to execute the API call. + +You can also execute the entire collection to get the results automatically: +[Run a +collection](https://learning.postman.com/docs/running-collections/intro-to-collection-runs/) + +## Run with Newman + +[Newman](https://learning.postman.com/docs/running-collections/using-newman-cli/command-line-integration-with-newman/) +is a CLI tool to execute Postman collections from the command line. To execute, +install globally ```npm i newman -g``` and execute the following command: + +```shell +newman run '.\Admin API E2E.postman_collection.json' -e '.\Admin API.postman_environment.json' -k +``` + +-k flag is only required when running with local certificate to ignore SSL +errors. + +## Adding new API calls + +To add a new API call, this can be done manually or importing from the generated +*swagger.json* file and adding tests and examples into the existing collection. +This can then be exported into .json format to be executed from the command +line. + +## Tips + +* If an error occurs and stops execution of the tests, you may need to manually + clear out the `TOKEN` variable: click the "..." button for "Admin API E2E" in + the collection pane; select "Edit"; click on the "Variables" tab; clear out + the value for `TOKEN`. +* The tests are designed to be run as a suite, rather than running them + individually. To run individually, it might be useful to modify (temporarily) + the Register step so that it uses a known client ID and client secret, instead + of using variables. diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/.automation_api_idp_mssql.env b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/.automation_api_idp_mssql.env new file mode 100644 index 000000000..ce2d1022d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/.automation_api_idp_mssql.env @@ -0,0 +1,49 @@ +API_MODE=SharedInstance +ODS_VIRTUAL_NAME=api +ADMIN_API_VIRTUAL_NAME=adminapi +LOGS_FOLDER=/tmp/logs +ADMINAPI_MODE=V2 + +# For Authentication +ISSUER_URL=https://localhost/${ADMIN_API_VIRTUAL_NAME} +SIGNING_KEY=TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0= + +IGNORES_CERTIFICATE_ERRORS=true +ENABLE_APPLICATION_RESET_ENDPOINT=true +EDFI_API_DISCOVERY_URL=http://ed-fi-ods-api:80/api/ + +PAGING_OFFSET=0 +PAGING_LIMIT=25 + +SQLSERVER_PORT=5432 + +# The following needs to be set to specify the ODS API endpoint for Admin API to internally connect. +# If user chooses direct connection between ODS API and Admin API within docker network, then set the api internal url as follows +API_INTERNAL_URL=http://${ODS_VIRTUAL_NAME} + +ADMIN_API_HEALTHCHECK_TEST="wget -nv -t1 --spider http://${ADMIN_API_VIRTUAL_NAME}/health || exit 1" + +# IdP db keycloak +KEYCLOAK_DB_IMAGE_TAG=16.2 +KEYCLOAK_POSTGRES_DB=keycloak_db +KEYCLOAK_POSTGRES_USER=edfi +KEYCLOAK_POSTGRES_PASSWORD=P@55w0rd +# IdP keycloak +KEYCLOAK_IMAGE_TAG=26.0 +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_PORT=28080 +KEYCLOAK_VIRTUAL_NAME=keycloak +KEYCLOAK_HOSTNAME= localhost +KEYCLOAK_HOSTNAME_PORT=443 +KEYCLOAK_HOSTNAME_STRICT_BACKCHANNEL=false +KEYCLOAK_HTTP_ENABLED=true +KEYCLOAK_HOSTNAME_STRICT_HTTPS=true +KEYCLOAK_HEALTH_ENABLED=true +KEYCLOAK_ADMIN_CONSOLE_REALM=edfi-admin-console + + +# Ods Api +TAG=7.3 +API_HEALTHCHECK_TEST="curl -f http://localhost/health" +ODS_CONNECTION_STRING_ENCRYPTION_KEY=6VDn8N4Kj7vYAmnMrFUgdXTqOaTiGZJc9Kf2TFFDHAA= diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/.automation_api_idp_pgsql.env b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/.automation_api_idp_pgsql.env new file mode 100644 index 000000000..3228e1b55 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/.automation_api_idp_pgsql.env @@ -0,0 +1,49 @@ +API_MODE=SharedInstance +ODS_VIRTUAL_NAME=api +ADMIN_API_VIRTUAL_NAME=adminapi +LOGS_FOLDER=/tmp/logs +ADMINAPI_MODE=V2 + +# For Authentication +ISSUER_URL=https://localhost/${ADMIN_API_VIRTUAL_NAME} +SIGNING_KEY=TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0= + +IGNORES_CERTIFICATE_ERRORS=true +ENABLE_APPLICATION_RESET_ENDPOINT=true +EDFI_API_DISCOVERY_URL=http://ed-fi-ods-api:80/api/ + +# For Postgres only +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_PORT=5432 + +# The following needs to be set to specify the ODS API endpoint for Admin API to internally connect. +# If user chooses direct connection between ODS API and Admin API within docker network, then set the api internal url as follows +API_INTERNAL_URL=http://${ODS_VIRTUAL_NAME} + +ADMIN_API_HEALTHCHECK_TEST="wget -nv -t1 --spider http://${ADMIN_API_VIRTUAL_NAME}/health || exit 1" + +# IdP db keycloak +KEYCLOAK_DB_IMAGE_TAG=16.2 +KEYCLOAK_POSTGRES_DB=keycloak_db +KEYCLOAK_POSTGRES_USER=edfi +KEYCLOAK_POSTGRES_PASSWORD=P@55w0rd +# IdP keycloak +KEYCLOAK_IMAGE_TAG=26.0 +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_PORT=28080 +KEYCLOAK_VIRTUAL_NAME=keycloak +KEYCLOAK_HOSTNAME= localhost +KEYCLOAK_HOSTNAME_PORT=443 +KEYCLOAK_HOSTNAME_STRICT_BACKCHANNEL=false +KEYCLOAK_HTTP_ENABLED=true +KEYCLOAK_HOSTNAME_STRICT_HTTPS=true +KEYCLOAK_HEALTH_ENABLED=true +KEYCLOAK_ADMIN_CONSOLE_REALM=edfi-admin-console + + +# Ods Api +TAG=7.3 +API_HEALTHCHECK_TEST="curl -f http://localhost/health" +ODS_CONNECTION_STRING_ENCRYPTION_KEY=6VDn8N4Kj7vYAmnMrFUgdXTqOaTiGZJc9Kf2TFFDHAA= diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/.automation_mssql.env b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/.automation_mssql.env new file mode 100644 index 000000000..343576df2 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/.automation_mssql.env @@ -0,0 +1,33 @@ +API_MODE=SharedInstance +ODS_VIRTUAL_NAME=api +ADMIN_API_VIRTUAL_NAME=adminapi +ADMIN_API_VERSION= 2.2.2-alpha.0.78 +LOGS_FOLDER=/tmp/logs +ADMINAPI_MODE=V2 + +# For Authentication +ISSUER_URL=https://localhost/${ADMIN_API_VIRTUAL_NAME} +SIGNING_KEY=TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0= + +IGNORES_CERTIFICATE_ERRORS=true +ENABLE_APPLICATION_RESET_ENDPOINT=true +EDFI_API_DISCOVERY_URL=http://ed-fi-ods-api:80/${ODS_VIRTUAL_NAME}/ + +PAGING_OFFSET=0 +PAGING_LIMIT=25 + +SQLSERVER_USER=edfi +SQLSERVER_PASSWORD=P@55w0rd +SQLSERVER_PORT=5432 + +# The following needs to be set to specify the ODS API endpoint for Admin API to internally connect. +# If user chooses direct connection between ODS API and Admin API within docker network, then set the api internal url as follows +API_INTERNAL_URL=http://${ODS_VIRTUAL_NAME} + +ADMIN_API_HEALTHCHECK_TEST="wget -nv -t1 --spider http://${ADMIN_API_VIRTUAL_NAME}/health || exit 1" + + +# Ods Api +TAG=7.3 +API_HEALTHCHECK_TEST="curl -f http://localhost/health" +ODS_CONNECTION_STRING_ENCRYPTION_KEY=6VDn8N4Kj7vYAmnMrFUgdXTqOaTiGZJc9Kf2TFFDHAA= diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/.automation_pgsql.env b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/.automation_pgsql.env new file mode 100644 index 000000000..d670d6a90 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/.automation_pgsql.env @@ -0,0 +1,31 @@ +API_MODE=SharedInstance +ODS_VIRTUAL_NAME=api +ADMIN_API_VERSION= 2.2.2-alpha.0.78 +ADMIN_API_VIRTUAL_NAME=adminapi +LOGS_FOLDER=/tmp/logs +ADMINAPI_MODE=V2 + +# For Authentication +ISSUER_URL=https://localhost/${ADMIN_API_VIRTUAL_NAME} +SIGNING_KEY=TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0= + +IGNORES_CERTIFICATE_ERRORS=true +ENABLE_APPLICATION_RESET_ENDPOINT=true +EDFI_API_DISCOVERY_URL=http://ed-fi-ods-api:80/${ODS_VIRTUAL_NAME}/ + +# For Postgres only +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_PORT=5432 + +# The following needs to be set to specify the ODS API endpoint for Admin API to internally connect. +# If user chooses direct connection between ODS API and Admin API within docker network, then set the api internal url as follows +API_INTERNAL_URL=http://${ODS_VIRTUAL_NAME} + +ADMIN_API_HEALTHCHECK_TEST="wget -nv -t1 --spider http://${ADMIN_API_VIRTUAL_NAME}/health || exit 1" + + +# Ods Api +TAG=7.3 +API_HEALTHCHECK_TEST="curl -f http://localhost/health" +ODS_CONNECTION_STRING_ENCRYPTION_KEY=6VDn8N4Kj7vYAmnMrFUgdXTqOaTiGZJc9Kf2TFFDHAA= diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/admin_inspect.sh b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/admin_inspect.sh new file mode 100644 index 000000000..c15910b75 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/admin_inspect.sh @@ -0,0 +1,28 @@ +#! /bin/bash +end=$((SECONDS+ 5 * 60)) + +echo $1 + +until [[ "`docker inspect -f {{.State.Health.Status}} $1`" == "healthy" || $SECONDS -gt $end ]]; do + sleep 2; +done; + +if [ "`docker inspect -f {{.State.Health.Status}} $1`" == "healthy" ] +then + echo "--- Container is healthy ---" +else + docker ps + docker logs adminapi --tail 50 + echo "--- Operation timed out. Review container status ---" + exit 1 +fi + +status=`wget -nv -t1 --spider -S https://localhost/adminapi/health --no-check-certificate 2>&1|grep "HTTP/"|awk '{print $2}'` + +if [[ $status -eq "200" ]] +then + echo "--- Admin API application is running ---" +else + echo "--- Admin API application is failing with status code ${status}" + exit 2 +fi diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/ods_inspect.sh b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/ods_inspect.sh new file mode 100644 index 000000000..91b580605 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/gh-action-setup/ods_inspect.sh @@ -0,0 +1,27 @@ +#! /bin/bash +end=$((SECONDS+ 5 * 60)) + +echo $1 + +until [[ "`docker inspect -f {{.State.Health.Status}} $1`" == "healthy" || $SECONDS -gt $end ]]; do + sleep 2; +done; + +if [ "`docker inspect -f {{.State.Health.Status}} $1`" == "healthy" ] +then + echo "--- Container is healthy ---" +else + docker ps + docker logs ed-fi-ods-api --tail 50 + echo "--- Operation timed out. Review container status ---" + exit 1 +fi + +status=`curl -s -o /dev/null -w "%{http_code}" -k https://localhost/api/health` +if [[ $status -eq "200" ]] +then + echo "--- Ods API application is running ---" +else + echo "--- Ods API application is failing with status code ${status}" + exit 2 +fi \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/gh-action-setup/.gitignore b/Application/EdFi.Ods.AdminApi/E2E Tests/gh-action-setup/.gitignore deleted file mode 100644 index 58df30fc6..000000000 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/gh-action-setup/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!.automation.env diff --git a/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj b/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj index d9f3ddbd8..77491a8f0 100644 --- a/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj +++ b/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj @@ -1,47 +1,57 @@ - - - net8.0 - enable - enable - true - NU5100, NU5124 - Linux - ../.. - dev.pgsql.Dockerfile - --no-cache - .env - adminapi-dev - - - - - - - - - - - - - - - Compatability - - - - - - - - - - - - - - - - - - - + + + net8.0 + enable + enable + true + NU5100, NU5124 + Linux + ../.. + dev.pgsql.Dockerfile + --no-cache + .env + adminapi-dev + true + a39ca29f-5ebf-412e-bca9-53fc7221286b + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.nuspec b/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.nuspec index df2c5fc04..edf9d9a47 100644 --- a/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.nuspec +++ b/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.nuspec @@ -14,7 +14,6 @@ - diff --git a/Application/EdFi.Ods.AdminApi/Features/Actions/ActionModel.cs b/Application/EdFi.Ods.AdminApi/Features/Actions/ActionModel.cs new file mode 100644 index 000000000..3708a1df0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Actions/ActionModel.cs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.Actions; + +[SwaggerSchema(Title = "Action")] +public class ActionModel +{ + public int Id { get; set; } + public string? Name { get; set; } + public string? Uri { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Actions/ReadActions.cs b/Application/EdFi.Ods.AdminApi/Features/Actions/ReadActions.cs new file mode 100644 index 000000000..f4acb9440 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Actions/ReadActions.cs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; + +namespace EdFi.Ods.AdminApi.Features.Actions; + +public class ReadActions : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/actions", GetActions) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static Task GetActions(IGetAllActionsQuery getAllActionsQuery, IMapper mapper, int? offset, int? limit, string? orderBy, string? direction, int? id, string? name) + { + var actions = mapper.Map>( + getAllActionsQuery.Execute( + new CommonQueryParams(offset, limit, orderBy, direction), + id, + name)); + return Task.FromResult(Results.Ok(actions)); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/AdminApiModeValidationMiddleware.cs b/Application/EdFi.Ods.AdminApi/Features/AdminApiModeValidationMiddleware.cs new file mode 100644 index 000000000..9e5ede895 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/AdminApiModeValidationMiddleware.cs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; +using System.Text.Json; +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.Common.Settings; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Features; + +public class AdminApiModeValidationMiddleware +{ + private readonly RequestDelegate _next; + private readonly AdminApiMode _adminApiMode; + + public AdminApiModeValidationMiddleware(RequestDelegate next, IOptions options) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + _adminApiMode = Enum.Parse(options.Value.AdminApiMode ?? AdminApiMode.V2.ToString(), true); + } + + public async Task InvokeAsync(HttpContext context) + { + var response = context.Response; + var path = context.Request.Path.Value ?? string.Empty; + + if (IsUnversionedEndpoint(path)) + { + await _next(context); + return; + } + + var requestedVersion = GetVersionFromPath(path); + + if (requestedVersion != _adminApiMode && !path.Contains("/swagger/")) + { + response.StatusCode = (int)HttpStatusCode.BadRequest; + await response.WriteAsync(JsonSerializer.Serialize(new { message = "Wrong API version for this instance mode." })); + return; + } + + await _next(context); + } + + private static bool IsUnversionedEndpoint(string path) + { + if (path is null) + throw new ArgumentNullException(nameof(path)); + + return GetVersionFromPath(path) == AdminApiMode.Unversioned; + } + + private static AdminApiMode GetVersionFromPath(string path) + { + if (path is null) + throw new ArgumentNullException(nameof(path)); + + return path switch + { + var p when p.Contains("/v2/", StringComparison.OrdinalIgnoreCase) => AdminApiMode.V2, + var p when p.Contains("/v1/", StringComparison.OrdinalIgnoreCase) => AdminApiMode.V1, + _ => AdminApiMode.Unversioned + }; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ApiClients/AddApiClient.cs b/Application/EdFi.Ods.AdminApi/Features/ApiClients/AddApiClient.cs new file mode 100644 index 000000000..d1a93fdd7 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ApiClients/AddApiClient.cs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.ApiClients; + +public class AddApiClient : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPost(endpoints, "/apiclients", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(201)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task Handle(Validator validator, IAddApiClientCommand addApiClientCommand, IMapper mapper, IUsersContext db, AddApiClientRequest request, IOptions options) + { + await validator.GuardAsync(request); + GuardAgainstInvalidEntityReferences(request, db); + var addedApiClientResult = addApiClientCommand.Execute(request, options); + var model = mapper.Map(addedApiClientResult); + return Results.Created($"/apiclients/{model.Id}", model); + } + + private static void GuardAgainstInvalidEntityReferences(AddApiClientRequest request, IUsersContext db) + { + if (null == db.Applications.Find(request.ApplicationId)) + throw new ValidationException([new ValidationFailure(nameof(request.ApplicationId), $"Application with ID {request.ApplicationId} not found.")]); + + ValidateOdsInstanceIds(request, db); + } + + private static void ValidateOdsInstanceIds(AddApiClientRequest request, IUsersContext db) + { + var allOdsInstanceIds = db.OdsInstances.Select(p => p.OdsInstanceId).ToList(); + + if ((request.OdsInstanceIds != null && request.OdsInstanceIds.Any()) && allOdsInstanceIds.Count == 0) + { + throw new ValidationException([new ValidationFailure(nameof(request.OdsInstanceIds), $"The following OdsInstanceIds were not found in database: {string.Join(", ", request.OdsInstanceIds)}")]); + } + + if ((request.OdsInstanceIds != null && request.OdsInstanceIds.Any()) && (!request.OdsInstanceIds.All(p => allOdsInstanceIds.Contains(p)))) + { + var notExist = request.OdsInstanceIds.Where(p => !allOdsInstanceIds.Contains(p)); + throw new ValidationException([new ValidationFailure(nameof(request.OdsInstanceIds), $"The following OdsInstanceIds were not found in database: {string.Join(", ", notExist)}")]); + } + } + + [SwaggerSchema(Title = "AddApiClientRequest")] + public class AddApiClientRequest : IAddApiClientModel + { + [SwaggerSchema(Description = FeatureConstants.ApiClientNameDescription, Nullable = false)] + public string Name { get; set; } = string.Empty; + + [SwaggerSchema(Description = FeatureConstants.ApiClientIsApprovedDescription, Nullable = false)] + public bool IsApproved { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ApiClientApplicationIdDescription, Nullable = false)] + public int ApplicationId { get; set; } + + [SwaggerSchema(Description = FeatureConstants.OdsInstanceIdsDescription, Nullable = false)] + public IEnumerable? OdsInstanceIds { get; set; } + } + + public class Validator : AbstractValidator + { + //Since this is a PoC, we are not implementing the full validation logic. + public Validator() + { + RuleFor(m => m.Name) + .NotEmpty(); + + RuleFor(m => m.Name) + .Must(BeWithinApiClientNameMaxLength) + .WithMessage(FeatureConstants.ApiClientNameLengthValidationMessage) + .When(x => x.Name != null); + + RuleFor(m => m.ApplicationId) + .GreaterThan(0); + + RuleFor(m => m.OdsInstanceIds) + .NotEmpty() + .WithMessage(FeatureConstants.OdsInstanceIdsValidationMessage); + + static bool BeWithinApiClientNameMaxLength(IAddApiClientModel model, string? name, ValidationContext context) + { + var extraCharactersInName = name!.Length - ValidationConstants.MaximumApiClientNameLength; + if (extraCharactersInName <= 0) + { + return true; + } + context.MessageFormatter.AppendArgument("Name", name); + context.MessageFormatter.AppendArgument("ExtraCharactersInName", extraCharactersInName); + return false; + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ApiClients/ApiClientModel.cs b/Application/EdFi.Ods.AdminApi/Features/ApiClients/ApiClientModel.cs new file mode 100644 index 000000000..5c79c9ed3 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ApiClients/ApiClientModel.cs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.ApiClients; + +[SwaggerSchema(Title = "ApiClient")] +public class ApiClientModel +{ + public int Id { get; set; } + public string Key { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public bool IsApproved { get; set; } = true; + public bool UseSandbox { get; set; } = false; + public int SandboxType { get; set; } = 0; + public int ApplicationId { get; set; } = 0; + public string KeyStatus { get; set; } = "Active"; + public IList? EducationOrganizationIds { get; set; } + public IList? OdsInstanceIds { get; set; } +} + +[SwaggerSchema(Title = "ApiClient")] +public class ApiClientResult +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Key { get; set; } = string.Empty; + public string Secret { get; set; } = string.Empty; + public int ApplicationId { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ApiClients/DeleteApiClient.cs b/Application/EdFi.Ods.AdminApi/Features/ApiClients/DeleteApiClient.cs new file mode 100644 index 000000000..539161ce7 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ApiClients/DeleteApiClient.cs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +namespace EdFi.Ods.AdminApi.Features.ApiClients; + +public class DeleteApiClient : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapDelete(endpoints, "/apiclients/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureCommonConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static Task Handle(IDeleteApiClientCommand deleteApiClientCommand, int id) + { + deleteApiClientCommand.Execute(id); + return Task.FromResult(Results.Ok("ApiClient".ToJsonObjectResponseDeleted())); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ApiClients/EditApiClient.cs b/Application/EdFi.Ods.AdminApi/Features/ApiClients/EditApiClient.cs new file mode 100644 index 000000000..add47a761 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ApiClients/EditApiClient.cs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Documentation; +using FluentValidation; +using FluentValidation.Results; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.ApiClients; + +public class EditApiClient : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPut(endpoints, "/apiclients/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task Handle(IEditApiClientCommand editApiClientCommand, IMapper mapper, + Validator validator, IUsersContext db, EditApiClientRequest request, int id) + { + request.Id = id; + await validator.GuardAsync(request); + GuardAgainstInvalidEntityReferences(request, db); + editApiClientCommand.Execute(request); + return Results.Ok(); + } + + private static void GuardAgainstInvalidEntityReferences(EditApiClientRequest request, IUsersContext db) + { + ValidateOdsInstanceIds(request, db); + } + + private static void ValidateOdsInstanceIds(EditApiClientRequest request, IUsersContext db) + { + var allOdsInstanceIds = db.OdsInstances.Select(p => p.OdsInstanceId).ToList(); + + if ((request.OdsInstanceIds != null && request.OdsInstanceIds.Any()) && allOdsInstanceIds.Count == 0) + { + throw new ValidationException(new[] { new ValidationFailure(nameof(request.OdsInstanceIds), $"The following OdsInstanceIds were not found in database: {string.Join(", ", request.OdsInstanceIds)}") }); + } + + if ((request.OdsInstanceIds != null && request.OdsInstanceIds.Any()) && (!request.OdsInstanceIds.All(p => allOdsInstanceIds.Contains(p)))) + { + var notExist = request.OdsInstanceIds.Where(p => !allOdsInstanceIds.Contains(p)); + throw new ValidationException(new[] { new ValidationFailure(nameof(request.OdsInstanceIds), $"The following OdsInstanceIds were not found in database: {string.Join(", ", notExist)}") }); + } + } + + [SwaggerSchema(Title = "EditApiClientRequest")] + public class EditApiClientRequest : IEditApiClientModel + { + [SwaggerExclude] + public int Id { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ApiClientNameDescription, Nullable = false)] + public string Name { get; set; } = string.Empty; + + [SwaggerSchema(Description = FeatureConstants.ApiClientIsApprovedDescription, Nullable = false)] + public bool IsApproved { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ApiClientApplicationIdDescription, Nullable = false)] + public int ApplicationId { get; set; } + + [SwaggerSchema(Description = FeatureConstants.OdsInstanceIdsDescription, Nullable = false)] + public IEnumerable? OdsInstanceIds { get; set; } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(m => m.Id) + .NotEmpty(); + + RuleFor(m => m.Name) + .NotEmpty(); + + RuleFor(m => m.Name) + .Must(BeWithinApiClientNameMaxLength) + .WithMessage(FeatureConstants.ApiClientNameLengthValidationMessage) + .When(x => x.Name != null); + + RuleFor(m => m.ApplicationId) + .GreaterThan(0); + + RuleFor(m => m.OdsInstanceIds) + .NotEmpty() + .WithMessage(FeatureConstants.OdsInstanceIdsValidationMessage); + + static bool BeWithinApiClientNameMaxLength(IEditApiClientModel model, string? name, ValidationContext context) + { + var extraCharactersInName = name!.Length - ValidationConstants.MaximumApiClientNameLength; + if (extraCharactersInName <= 0) + { + return true; + } + context.MessageFormatter.AppendArgument("Name", name); + context.MessageFormatter.AppendArgument("ExtraCharactersInName", extraCharactersInName); + return false; + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ApiClients/ReadApiClient.cs b/Application/EdFi.Ods.AdminApi/Features/ApiClients/ReadApiClient.cs new file mode 100644 index 000000000..fcbaf7798 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ApiClients/ReadApiClient.cs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using Microsoft.AspNetCore.Mvc; + +namespace EdFi.Ods.AdminApi.Features.ApiClients; + +public class ReadApiClient : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/apiclients", GetApiClients) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + + AdminApiEndpointBuilder.MapGet(endpoints, "/apiclients/{id}", GetApiClient) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static Task GetApiClients( + [FromServices] IGetApiClientsByApplicationIdQuery getApiClientsByApplicationIdQuery, + [FromServices] IMapper mapper, + [FromQuery(Name = "applicationid")] int applicationid) + { + var apiClients = mapper.Map>(getApiClientsByApplicationIdQuery.Execute(applicationid)); + return Task.FromResult(Results.Ok(apiClients)); + } + + public static Task GetApiClient([ + FromServices] IGetApiClientByIdQuery getApiClientByIdQuery, + [FromServices] IMapper mapper, + int id) + { + var apiClient = getApiClientByIdQuery.Execute(id) ?? throw new NotFoundException("apiClient", id); + var model = mapper.Map(apiClient); + return Task.FromResult(Results.Ok(model)); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ApiClients/ResetApiClientCredentials.cs b/Application/EdFi.Ods.AdminApi/Features/ApiClients/ResetApiClientCredentials.cs new file mode 100644 index 000000000..3c852b385 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ApiClients/ResetApiClientCredentials.cs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +namespace EdFi.Ods.AdminApi.Features.ApiClients; + +public class ResetApiClientCredentials : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPut(endpoints, "/apiclients/{id}/reset-credential", HandleResetCredentials) + .WithSummary("Reset apiclient credentials. Returns new key and secret.") + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task HandleResetCredentials(IRegenerateApiClientSecretCommand resetSecretCommand, IMapper mapper, int id) + { + var resetCredentials = await Task.Run(() => resetSecretCommand.Execute(id)); + var model = mapper.Map(resetCredentials); + return Results.Ok(model); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Applications/AddApplication.cs b/Application/EdFi.Ods.AdminApi/Features/Applications/AddApplication.cs index 30edd8474..d86b4957c 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Applications/AddApplication.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Applications/AddApplication.cs @@ -1,108 +1,147 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using AutoMapper; -using EdFi.Admin.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Infrastructure; -using EdFi.Ods.AdminApi.Infrastructure.Documentation; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using FluentValidation; -using Swashbuckle.AspNetCore.Annotations; -using FluentValidation.Results; -using EdFi.Ods.AdminApi.Infrastructure.Commands; - -namespace EdFi.Ods.AdminApi.Features.Applications; - -public class AddApplication : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder.MapPost(endpoints, "/applications", Handle) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponse(201)) - .BuildForVersions(AdminApiVersions.V1); - } - - public async Task Handle(Validator validator, IAddApplicationCommand addApplicationCommand, IMapper mapper, IUsersContext db, Request request) - { - await validator.GuardAsync(request); - GuardAgainstInvalidEntityReferences(request, db); - var addedApplicationResult = addApplicationCommand.Execute(request); - var model = mapper.Map(addedApplicationResult); - return AdminApiResponse.Created(model, "Application", $"/applications/{model.ApplicationId}"); - } - - private void GuardAgainstInvalidEntityReferences(Request request, IUsersContext db) - { - if (null == db.Vendors.Find(request.VendorId)) - throw new ValidationException(new[] { new ValidationFailure(nameof(request.VendorId), $"Vendor with ID {request.VendorId} not found.") }); - - if (request.ProfileId.HasValue && db.Profiles.Find(request.ProfileId) == null) - throw new ValidationException(new[] { new ValidationFailure(nameof(request.ProfileId), $"Profile with ID {request.ProfileId} not found.") }); - - if (request.OdsInstanceId.HasValue && db.OdsInstances.Find(request.OdsInstanceId) == null) - throw new ValidationException(new[] { new ValidationFailure(nameof(request.OdsInstanceId), $"Ods instance with ID {request.OdsInstanceId} not found.") }); - } - - [SwaggerSchema(Title = "AddApplicationRequest")] - public class Request : IAddApplicationModel - { - [SwaggerSchema(Description = FeatureConstants.ApplicationNameDescription, Nullable = false)] - public string? ApplicationName { get; set; } - - [SwaggerSchema(Description = FeatureConstants.VendorIdDescription, Nullable = false)] - public int VendorId { get; set; } - - [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] - public string? ClaimSetName { get; set; } - - [SwaggerOptional] - [SwaggerSchema(Description = FeatureConstants.ProfileIdDescription)] - public int? ProfileId { get; set; } - - [SwaggerOptional] - [SwaggerSchema(Description = FeatureConstants.OdsInstanceIdDescription)] - public int? OdsInstanceId { get; set; } - - [SwaggerSchema(Description = FeatureConstants.EducationOrganizationIdsDescription, Nullable = false)] - public IEnumerable? EducationOrganizationIds { get; set; } - } - - public class Validator : AbstractValidator - { - public Validator() - { - RuleFor(m => m.ApplicationName) - .NotEmpty(); - - RuleFor(m => m.ApplicationName) - .Must(BeWithinApplicationNameMaxLength) - .WithMessage(FeatureConstants.ApplicationNameLengthValidationMessage) - .When(x => x.ApplicationName != null); - - RuleFor(m => m.ClaimSetName) - .NotEmpty() - .WithMessage(FeatureConstants.ClaimSetNameValidationMessage); - - RuleFor(m => m.EducationOrganizationIds) - .NotEmpty() - .WithMessage(FeatureConstants.EdOrgIdsValidationMessage); - - RuleFor(m => m.VendorId).Must(id => id > 0).WithMessage(FeatureConstants.VendorIdValidationMessage); - } - - private bool BeWithinApplicationNameMaxLength(IAddApplicationModel model, string? applicationName, ValidationContext context) - { - var extraCharactersInName = applicationName!.Length - ValidationConstants.MaximumApplicationNameLength; - if (extraCharactersInName <= 0) - { - return true; - } - context.MessageFormatter.AppendArgument("ApplicationName", applicationName); - context.MessageFormatter.AppendArgument("ExtraCharactersInName", extraCharactersInName); - return false; - } - } -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Documentation; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.Applications; + +public class AddApplication : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPost(endpoints, "/applications", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(201)) + .BuildForVersions(string.Empty, true, AdminApiVersions.V2); + } + + public static async Task Handle(Validator validator, IAddApplicationCommand addApplicationCommand, IMapper mapper, IUsersContext db, AddApplicationRequest request, IOptions options) + { + await validator.GuardAsync(request); + GuardAgainstInvalidEntityReferences(request, db); + var addedApplicationResult = addApplicationCommand.Execute(request, options); + var model = mapper.Map(addedApplicationResult); + return Results.Created($"/applications/{model.Id}", model); + } + + private static void GuardAgainstInvalidEntityReferences(AddApplicationRequest request, IUsersContext db) + { + if (null == db.Vendors.Find(request.VendorId)) + throw new ValidationException(new[] { new ValidationFailure(nameof(request.VendorId), $"Vendor with ID {request.VendorId} not found.") }); + + ValidateProfileIds(request, db); + ValidateOdsInstanceIds(request, db); + } + + private static void ValidateProfileIds(AddApplicationRequest request, IUsersContext db) + { + var allProfileIds = db.Profiles.Select(p => p.ProfileId).ToList(); + + if ((request.ProfileIds != null && request.ProfileIds.Any()) && allProfileIds.Count == 0) + { + throw new ValidationException(new[] { new ValidationFailure(nameof(request.ProfileIds), $"The following ProfileIds were not found in database: {string.Join(", ", request.ProfileIds)}") }); + } + + if ((request.ProfileIds != null && request.ProfileIds.Any()) && (!request.ProfileIds.All(p => allProfileIds.Contains(p)))) + { + var notExist = request.ProfileIds.Where(p => !allProfileIds.Contains(p)); + throw new ValidationException(new[] { new ValidationFailure(nameof(request.ProfileIds), $"The following ProfileIds were not found in database: {string.Join(", ", notExist)}") }); + } + } + + private static void ValidateOdsInstanceIds(AddApplicationRequest request, IUsersContext db) + { + var allOdsInstanceIds = db.OdsInstances.Select(p => p.OdsInstanceId).ToList(); + + if ((request.OdsInstanceIds != null && request.OdsInstanceIds.Any()) && allOdsInstanceIds.Count == 0) + { + throw new ValidationException(new[] { new ValidationFailure(nameof(request.OdsInstanceIds), $"The following OdsInstanceIds were not found in database: {string.Join(", ", request.OdsInstanceIds)}") }); + } + + if ((request.OdsInstanceIds != null && request.OdsInstanceIds.Any()) && (!request.OdsInstanceIds.All(p => allOdsInstanceIds.Contains(p)))) + { + var notExist = request.OdsInstanceIds.Where(p => !allOdsInstanceIds.Contains(p)); + throw new ValidationException(new[] { new ValidationFailure(nameof(request.OdsInstanceIds), $"The following OdsInstanceIds were not found in database: {string.Join(", ", notExist)}") }); + } + } + + [SwaggerSchema(Title = "AddApplicationRequest")] + public class AddApplicationRequest : IAddApplicationModel + { + [SwaggerSchema(Description = FeatureConstants.ApplicationNameDescription, Nullable = false)] + public string? ApplicationName { get; set; } + + [SwaggerSchema(Description = FeatureConstants.VendorIdDescription, Nullable = false)] + public int VendorId { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] + public string? ClaimSetName { get; set; } + + [SwaggerOptional] + [SwaggerSchema(Description = FeatureConstants.ProfileIdDescription)] + public IEnumerable? ProfileIds { get; set; } + + [SwaggerSchema(Description = FeatureConstants.EducationOrganizationIdsDescription, Nullable = false)] + public IEnumerable? EducationOrganizationIds { get; set; } + + [SwaggerSchema(Description = FeatureConstants.OdsInstanceIdsDescription, Nullable = false)] + public IEnumerable? OdsInstanceIds { get; set; } + + [SwaggerOptional] + [SwaggerSchema(Description = FeatureConstants.Enable)] + public bool? Enabled { get; set; } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(m => m.ApplicationName) + .NotEmpty(); + + RuleFor(m => m.ApplicationName) + .Must(BeWithinApplicationNameMaxLength) + .WithMessage(FeatureConstants.ApplicationNameLengthValidationMessage) + .When(x => x.ApplicationName != null); + + RuleFor(m => m.ClaimSetName) + .NotEmpty() + .WithMessage(FeatureConstants.ClaimSetNameValidationMessage); + + RuleFor(m => m.EducationOrganizationIds) + .NotEmpty() + .WithMessage(FeatureConstants.EdOrgIdsValidationMessage); + + RuleFor(m => m.OdsInstanceIds) + .NotEmpty() + .WithMessage(FeatureConstants.OdsInstanceIdsValidationMessage); + + RuleFor(m => m.VendorId).Must(id => id > 0).WithMessage(FeatureConstants.VendorIdValidationMessage); + } + + private static bool BeWithinApplicationNameMaxLength(IAddApplicationModel model, string? applicationName, ValidationContext context) + { + var extraCharactersInName = applicationName!.Length - ValidationConstants.MaximumApplicationNameLength; + if (extraCharactersInName <= 0) + { + return true; + } + context.MessageFormatter.AppendArgument("ApplicationName", applicationName); + context.MessageFormatter.AppendArgument("ExtraCharactersInName", extraCharactersInName); + return false; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Applications/ApplicationModel.cs b/Application/EdFi.Ods.AdminApi/Features/Applications/ApplicationModel.cs index 4641ad3a0..de27e7e9d 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Applications/ApplicationModel.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Applications/ApplicationModel.cs @@ -1,36 +1,41 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using Swashbuckle.AspNetCore.Annotations; - -namespace EdFi.Ods.AdminApi.Features.Applications; - -[SwaggerSchema(Title = "Application")] -public class ApplicationModel -{ - public int ApplicationId { get; set; } - public string? ApplicationName { get; set; } - public string? ClaimSetName { get; set; } - public string? ProfileName { get; set; } - public IList? EducationOrganizationIds { get; set; } - public string? OdsInstanceName { get; set; } - public int? OdsInstanceId { get; set; } - public int? VendorId { get; set; } - public IList? Profiles { get; set; } -} - -[SwaggerSchema(Title = "ApplicationKeySecret")] -public class ApplicationResult -{ - public int ApplicationId { get; set; } - public string? Key { get; set; } - public string? Secret { get; set; } -} - -[SwaggerSchema(Title = "Profile")] -public class Profile -{ - public int? Id { get; set; } -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.Applications; + +[SwaggerSchema(Title = "Application")] +public class ApplicationModel +{ + public int Id { get; set; } + public string? ApplicationName { get; set; } + public string? ClaimSetName { get; set; } + public IList? EducationOrganizationIds { get; set; } + public int? VendorId { get; set; } + public IList? ProfileIds { get; set; } + public IList? OdsInstanceIds { get; set; } + public bool Enabled { get; set; } = true; +} + +[SwaggerSchema(Title = "Application")] +public class SimpleApplicationModel +{ + public string? ApplicationName { get; set; } +} + +[SwaggerSchema(Title = "ApplicationKeySecret")] +public class ApplicationResult +{ + public int Id { get; set; } + public string? Key { get; set; } + public string? Secret { get; set; } +} + +[SwaggerSchema(Title = "Profile")] +public class Profile +{ + public int? Id { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Applications/DeleteApplication.cs b/Application/EdFi.Ods.AdminApi/Features/Applications/DeleteApplication.cs index 36755066c..9271acc1d 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Applications/DeleteApplication.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Applications/DeleteApplication.cs @@ -3,9 +3,13 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; - +using EdFi.Ods.AdminApi.Infrastructure.Extensions; + namespace EdFi.Ods.AdminApi.Features.Applications; public class DeleteApplication : IFeature @@ -13,14 +17,14 @@ public class DeleteApplication : IFeature public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder.MapDelete(endpoints, "/applications/{id}", Handle) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponseCode(200, FeatureConstants.DeletedSuccessResponseDescription)) - .BuildForVersions(AdminApiVersions.V1); + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureCommonConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V2); } - public Task Handle(IDeleteApplicationCommand deleteApplicationCommand, int id) + public static Task Handle(IDeleteApplicationCommand deleteApplicationCommand, int id) { deleteApplicationCommand.Execute(id); - return Task.FromResult(AdminApiResponse.Deleted("Application")); + return Task.FromResult(Results.Ok("Application".ToJsonObjectResponseDeleted())); } } diff --git a/Application/EdFi.Ods.AdminApi/Features/Applications/EditApplication.cs b/Application/EdFi.Ods.AdminApi/Features/Applications/EditApplication.cs index f5a7f57e1..45dad5509 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Applications/EditApplication.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Applications/EditApplication.cs @@ -1,109 +1,146 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using AutoMapper; -using EdFi.Admin.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Infrastructure; -using EdFi.Ods.AdminApi.Infrastructure.Documentation; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using FluentValidation; -using FluentValidation.Results; -using Swashbuckle.AspNetCore.Annotations; -using EdFi.Ods.AdminApi.Infrastructure.Commands; - -namespace EdFi.Ods.AdminApi.Features.Applications; - -public class EditApplication : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder.MapPut(endpoints, "/applications/{id}", Handle) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V1); - } - - public async Task Handle(IEditApplicationCommand editApplicationCommand, IMapper mapper, - Validator validator, IUsersContext db, Request request, int id) - { - request.ApplicationId = id; - await validator.GuardAsync(request); - GuardAgainstInvalidEntityReferences(request, db); - - var updatedApplication = editApplicationCommand.Execute(request); - var model = mapper.Map(updatedApplication); - return AdminApiResponse.Updated(model, "Application"); - } - - private static void GuardAgainstInvalidEntityReferences(Request request, IUsersContext db) - { - if (null == db.Vendors.Find(request.VendorId)) - throw new ValidationException(new[] { new ValidationFailure(nameof(request.VendorId), $"Vendor with ID {request.VendorId} not found.") }); - - if (request.ProfileId.HasValue && db.Profiles.Find(request.ProfileId) == null) - throw new ValidationException(new[] { new ValidationFailure(nameof(request.ProfileId), $"Profile with ID {request.ProfileId} not found.") }); - } - - [SwaggerSchema(Title = "EditApplicationRequest")] - public class Request : IEditApplicationModel - { - [SwaggerSchema(Description = "Application id", Nullable = false)] - public int ApplicationId { get; set; } - - [SwaggerSchema(Description = FeatureConstants.ApplicationNameDescription, Nullable = false)] - public string? ApplicationName { get; set; } - - [SwaggerSchema(Description = FeatureConstants.VendorIdDescription, Nullable = false)] - public int VendorId { get; set; } - - [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] - public string? ClaimSetName { get; set; } - - [SwaggerOptional] - [SwaggerSchema(Description = FeatureConstants.ProfileIdDescription)] - public int? ProfileId { get; set; } - - [SwaggerOptional] - [SwaggerSchema(Description = FeatureConstants.OdsInstanceIdDescription)] - public int? OdsInstanceId { get; set; } - - [SwaggerSchema(Description = FeatureConstants.EducationOrganizationIdsDescription, Nullable = false)] - public IEnumerable? EducationOrganizationIds { get; set; } - } - - public class Validator : AbstractValidator - { - public Validator() - { - RuleFor(m => m.ApplicationId).NotEmpty(); - RuleFor(m => m.ApplicationName).NotEmpty(); - RuleFor(m => m.ApplicationName) - .Must(BeWithinApplicationNameMaxLength) - .WithMessage(FeatureConstants.ApplicationNameLengthValidationMessage) - .When(x => x.ApplicationName != null); - RuleFor(m => m.ClaimSetName) - .NotEmpty() - .WithMessage(FeatureConstants.ClaimSetNameValidationMessage); - - RuleFor(m => m.EducationOrganizationIds) - .NotEmpty() - .WithMessage(FeatureConstants.EdOrgIdsValidationMessage); - - RuleFor(m => m.VendorId).Must(id => id > 0).WithMessage(FeatureConstants.VendorIdValidationMessage); - - static bool BeWithinApplicationNameMaxLength(IEditApplicationModel model, string? applicationName, ValidationContext context) - { - var extraCharactersInName = applicationName!.Length - ValidationConstants.MaximumApplicationNameLength; - if (extraCharactersInName <= 0) - { - return true; - } - context.MessageFormatter.AppendArgument("ApplicationName", applicationName); - context.MessageFormatter.AppendArgument("ExtraCharactersInName", extraCharactersInName); - return false; - } - } - } -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Documentation; +using FluentValidation; +using FluentValidation.Results; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.Applications; + +public class EditApplication : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPut(endpoints, "/applications/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task Handle(IEditApplicationCommand editApplicationCommand, IMapper mapper, + Validator validator, IUsersContext db, EditApplicationRequest request, int id) + { + request.Id = id; + await validator.GuardAsync(request); + GuardAgainstInvalidEntityReferences(request, db); + editApplicationCommand.Execute(request); + return Results.Ok(); + } + + private static void GuardAgainstInvalidEntityReferences(EditApplicationRequest request, IUsersContext db) + { + if (null == db.Vendors.Find(request.VendorId)) + throw new ValidationException(new[] { new ValidationFailure(nameof(request.VendorId), $"Vendor with ID {request.VendorId} not found.") }); + + ValidateProfileIds(request, db); + ValidateOdsInstanceIds(request, db); + } + + private static void ValidateProfileIds(EditApplicationRequest request, IUsersContext db) + { + var allProfileIds = db.Profiles.Select(p => p.ProfileId).ToList(); + if ((request.ProfileIds != null && request.ProfileIds.Any()) && allProfileIds.Count == 0) + { + throw new ValidationException(new[] { new ValidationFailure(nameof(request.ProfileIds), $"The following ProfileIds were not found in database: {string.Join(", ", request.ProfileIds)}") }); + } + if ((request.ProfileIds != null && request.ProfileIds.Any()) && (!request.ProfileIds.All(p => allProfileIds.Contains(p)))) + { + var notExist = request.ProfileIds.Where(p => !allProfileIds.Contains(p)); + throw new ValidationException(new[] { new ValidationFailure(nameof(request.ProfileIds), $"The following ProfileIds were not found in database: {string.Join(", ", notExist)}") }); + } + } + + private static void ValidateOdsInstanceIds(EditApplicationRequest request, IUsersContext db) + { + var allOdsInstanceIds = db.OdsInstances.Select(p => p.OdsInstanceId).ToList(); + + if ((request.OdsInstanceIds != null && request.OdsInstanceIds.Any()) && allOdsInstanceIds.Count == 0) + { + throw new ValidationException(new[] { new ValidationFailure(nameof(request.OdsInstanceIds), $"The following OdsInstanceIds were not found in database: {string.Join(", ", request.OdsInstanceIds)}") }); + } + + if ((request.OdsInstanceIds != null && request.OdsInstanceIds.Any()) && (!request.OdsInstanceIds.All(p => allOdsInstanceIds.Contains(p)))) + { + var notExist = request.OdsInstanceIds.Where(p => !allOdsInstanceIds.Contains(p)); + throw new ValidationException(new[] { new ValidationFailure(nameof(request.OdsInstanceIds), $"The following OdsInstanceIds were not found in database: {string.Join(", ", notExist)}") }); + } + } + + [SwaggerSchema(Title = "EditApplicationRequest")] + public class EditApplicationRequest : IEditApplicationModel + { + [SwaggerExclude] + public int Id { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ApplicationNameDescription, Nullable = false)] + public string? ApplicationName { get; set; } + + [SwaggerSchema(Description = FeatureConstants.VendorIdDescription, Nullable = false)] + public int VendorId { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] + public string? ClaimSetName { get; set; } + + [SwaggerOptional] + [SwaggerSchema(Description = FeatureConstants.ProfileIdDescription)] + public IEnumerable? ProfileIds { get; set; } + + [SwaggerSchema(Description = FeatureConstants.EducationOrganizationIdsDescription, Nullable = false)] + public IEnumerable? EducationOrganizationIds { get; set; } + + [SwaggerSchema(Description = FeatureConstants.OdsInstanceIdsDescription, Nullable = false)] + public IEnumerable? OdsInstanceIds { get; set; } + + [SwaggerOptional] + [SwaggerSchema(Description = FeatureConstants.Enable)] + public bool? Enabled { get; set; } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(m => m.Id).NotEmpty(); + RuleFor(m => m.ApplicationName).NotEmpty(); + RuleFor(m => m.ApplicationName) + .Must(BeWithinApplicationNameMaxLength) + .WithMessage(FeatureConstants.ApplicationNameLengthValidationMessage) + .When(x => x.ApplicationName != null); + RuleFor(m => m.ClaimSetName) + .NotEmpty() + .WithMessage(FeatureConstants.ClaimSetNameValidationMessage); + + RuleFor(m => m.EducationOrganizationIds) + .NotEmpty() + .WithMessage(FeatureConstants.EdOrgIdsValidationMessage); + + RuleFor(m => m.OdsInstanceIds) + .NotEmpty() + .WithMessage(FeatureConstants.OdsInstanceIdsValidationMessage); + + RuleFor(m => m.VendorId).Must(id => id > 0).WithMessage(FeatureConstants.VendorIdValidationMessage); + + static bool BeWithinApplicationNameMaxLength(IEditApplicationModel model, string? applicationName, ValidationContext context) + { + var extraCharactersInName = applicationName!.Length - ValidationConstants.MaximumApplicationNameLength; + if (extraCharactersInName <= 0) + { + return true; + } + context.MessageFormatter.AppendArgument("ApplicationName", applicationName); + context.MessageFormatter.AppendArgument("ExtraCharactersInName", extraCharactersInName); + return false; + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Applications/ReadApplication.cs b/Application/EdFi.Ods.AdminApi/Features/Applications/ReadApplication.cs index e5b10c35f..c5f3cc600 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Applications/ReadApplication.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Applications/ReadApplication.cs @@ -1,44 +1,70 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using AutoMapper; -using EdFi.Ods.AdminApi.Infrastructure; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; - -namespace EdFi.Ods.AdminApi.Features.Applications; - -public class ReadApplication : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder.MapGet(endpoints, "/applications", GetApplications) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V1); - - AdminApiEndpointBuilder.MapGet(endpoints, "/applications/{id}", GetApplication) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V1); - } - - internal Task GetApplications( - IGetApplicationsQuery getApplicationsAndApplicationsQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams) - { - var applications = getApplicationsAndApplicationsQuery.Execute(commonQueryParams); - return Task.FromResult(AdminApiResponse>.Ok(mapper.Map>(applications))); - } - - internal Task GetApplication(GetApplicationByIdQuery getApplicationByIdQuery, IMapper mapper, int id) - { - var application = getApplicationByIdQuery.Execute(id); - if (application == null) - { - throw new NotFoundException("application", id); - } - var model = mapper.Map(application); - return Task.FromResult(AdminApiResponse.Ok(model)); - } -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Features.Applications; + +public class ReadApplication : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/applications", GetApplications) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + + AdminApiEndpointBuilder.MapGet(endpoints, "/applications/{id}", GetApplication) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static async Task GetApplications( + IGetAllApplicationsQuery getAllApplicationsQuery, + IMapper mapper, + IOptions settings, + Validator validator, + [AsParameters] CommonQueryParams commonQueryParams, int? id, string? applicationName, string? claimsetName, string? ids) + { + if (!string.IsNullOrEmpty(ids)) + { + await validator.GuardAsync(ids); + } + var applications = mapper.Map>(getAllApplicationsQuery.Execute(commonQueryParams, id, applicationName, claimsetName, ids)); + return Results.Ok(applications); + } + + internal static Task GetApplication(GetApplicationByIdQuery getApplicationByIdQuery, IMapper mapper, int id) + { + var application = getApplicationByIdQuery.Execute(id); + if (application == null) + { + throw new NotFoundException("application", id); + } + var model = mapper.Map(application); + return Task.FromResult(Results.Ok(model)); + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(ids => ids) + .Must(ids => Array.TrueForAll(ids.Split(','), id => int.TryParse(id.Trim(), out _))) + .WithMessage("The 'ids' query parameter must be a comma-separated list of integers."); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Applications/ReadApplicationsByOdsInstance.cs b/Application/EdFi.Ods.AdminApi/Features/Applications/ReadApplicationsByOdsInstance.cs new file mode 100644 index 000000000..c7487b516 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Applications/ReadApplicationsByOdsInstance.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +namespace EdFi.Ods.AdminApi.Features.Applications; + +public class ReadApplicationsByOdsInstance : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + var url = "odsInstances/{id}/applications"; + + AdminApiEndpointBuilder.MapGet(endpoints, url, GetOdsInstanceApplications) + .WithSummary("Retrieves applications assigned to a specific ODS instance based on the resource identifier.") + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static Task GetOdsInstanceApplications(IGetApplicationsByOdsInstanceIdQuery getApplicationByOdsInstanceIdQuery, IMapper mapper, int id) + { + var odsInstanceApplications = mapper.Map>(getApplicationByOdsInstanceIdQuery.Execute(id)); + return Task.FromResult(Results.Ok(odsInstanceApplications)); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Applications/ReadApplicationsByVendor.cs b/Application/EdFi.Ods.AdminApi/Features/Applications/ReadApplicationsByVendor.cs index a8785d4cd..4bb7d7772 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Applications/ReadApplicationsByVendor.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Applications/ReadApplicationsByVendor.cs @@ -4,6 +4,8 @@ // See the LICENSE and NOTICES files in the project root for more information. using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; @@ -16,14 +18,14 @@ public void MapEndpoints(IEndpointRouteBuilder endpoints) var url = "vendors/{id}/applications"; AdminApiEndpointBuilder.MapGet(endpoints, url, GetVendorApplications) - .WithDescription("Retrieves applications assigned to a specific vendor based on the resource identifier.") + .WithSummary("Retrieves applications assigned to a specific vendor based on the resource identifier.") .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V1); + .BuildForVersions(AdminApiVersions.V2); } - internal Task GetVendorApplications(GetApplicationsByVendorIdQuery getApplicationByVendorIdQuery, IMapper mapper, int id) + internal static Task GetVendorApplications(GetApplicationsByVendorIdQuery getApplicationByVendorIdQuery, IMapper mapper, int id) { var vendorApplications = mapper.Map>(getApplicationByVendorIdQuery.Execute(id)); - return Task.FromResult(AdminApiResponse>.Ok(vendorApplications)); + return Task.FromResult(Results.Ok(vendorApplications)); } } diff --git a/Application/EdFi.Ods.AdminApi/Features/Applications/ResetApplicationCredentials.cs b/Application/EdFi.Ods.AdminApi/Features/Applications/ResetApplicationCredentials.cs index f627899fd..fb1fd0aa9 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Applications/ResetApplicationCredentials.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Applications/ResetApplicationCredentials.cs @@ -3,9 +3,16 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using System.ComponentModel.DataAnnotations; using AutoMapper; -using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Settings; using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.Extensions.Options; namespace EdFi.Ods.AdminApi.Features.Applications; @@ -14,15 +21,18 @@ public class ResetApplicationCredentials : IFeature public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder.MapPut(endpoints, "/applications/{id}/reset-credential", HandleResetCredentials) - .WithDescription("Reset application credentials. Returns new key and secret.") + .WithSummary("Reset application credentials. Returns new key and secret.") .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V1); + .BuildForVersions(AdminApiVersions.V2); } - public async Task HandleResetCredentials(RegenerateApiClientSecretCommand resetSecretCommand, IMapper mapper, int id) + public static async Task HandleResetCredentials(RegenerateApplicationApiClientSecretCommand resetSecretCommand, IOptions settings, IMapper mapper, int id) { + if (!settings.Value.EnableApplicationResetEndpoint) + throw new FluentValidation.ValidationException(new[] { new ValidationFailure(nameof(Application), $"This endpoint has been disabled on application settings.") }); + var resetApplicationSecret = await Task.Run(() => resetSecretCommand.Execute(id)); var model = mapper.Map(resetApplicationSecret); - return AdminApiResponse.Updated(model, "Application secret"); + return Results.Ok(model); } } diff --git a/Application/EdFi.Ods.AdminApi/Features/AuthorizationStrategies/AuthorizationStrategyModel.cs b/Application/EdFi.Ods.AdminApi/Features/AuthorizationStrategies/AuthorizationStrategyModel.cs new file mode 100644 index 000000000..28c90b727 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/AuthorizationStrategies/AuthorizationStrategyModel.cs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. +using Swashbuckle.AspNetCore.Annotations; +using System.Text.Json.Serialization; + +namespace EdFi.Ods.AdminApi.Features.AuthorizationStrategies; + +[SwaggerSchema(Title = "AuthorizationStrategy")] +public class AuthorizationStrategyModel +{ + [JsonPropertyName("id")] + public int AuthStrategyId { get; set; } + [JsonPropertyName("name")] + public string? AuthStrategyName { get; set; } + + public string? DisplayName { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/AuthorizationStrategies/ReadAuthorizationStrategy.cs b/Application/EdFi.Ods.AdminApi/Features/AuthorizationStrategies/ReadAuthorizationStrategy.cs new file mode 100644 index 000000000..6abed89c3 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/AuthorizationStrategies/ReadAuthorizationStrategy.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; + +namespace EdFi.Ods.AdminApi.Features.AuthorizationStrategies; + +public class ReadAuthorizationStrategy : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/authorizationStrategies", GetAuthStrategies) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static Task GetAuthStrategies(IGetAuthStrategiesQuery getAuthStrategiesQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams) + { + var authStrategyList = mapper.Map>(getAuthStrategiesQuery.Execute(commonQueryParams)); + return Task.FromResult(Results.Ok(authStrategyList)); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/AddClaimSet.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/AddClaimSet.cs index 3f4ee8984..2913c7cd5 100644 --- a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/AddClaimSet.cs +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/AddClaimSet.cs @@ -3,26 +3,25 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using AutoMapper; +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using EdFi.Ods.AdminApi.Infrastructure.JsonContractResolvers; -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; -using FluentValidation; -using Newtonsoft.Json; -using Swashbuckle.AspNetCore.Annotations; - +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using FluentValidation; +using Swashbuckle.AspNetCore.Annotations; + namespace EdFi.Ods.AdminApi.Features.ClaimSets; public class AddClaimSet : IFeature { public void MapEndpoints(IEndpointRouteBuilder endpoints) { - AdminApiEndpointBuilder.MapPost(endpoints, "/claimsets", Handle) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponse(201)) - .BuildForVersions(AdminApiVersions.V1); + AdminApiEndpointBuilder.MapPost(endpoints, "/claimSets", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(201)) + .BuildForVersions(AdminApiVersions.V2); } public async Task Handle(Validator validator, AddClaimSetCommand addClaimSetCommand, @@ -30,97 +29,46 @@ public async Task Handle(Validator validator, AddClaimSetCommand addCla IGetClaimSetByIdQuery getClaimSetByIdQuery, IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, IGetApplicationsByClaimSetIdQuery getApplications, - IAuthStrategyResolver strategyResolver, - IOdsSecurityModelVersionResolver odsSecurityModelResolver, + IAuthStrategyResolver strategyResolver, IMapper mapper, - Request request) + AddClaimSetRequest request) { await validator.GuardAsync(request); var addedClaimSetId = addClaimSetCommand.Execute(new AddClaimSetModel { ClaimSetName = request.Name ?? string.Empty - }); - - var resourceClaims = mapper.Map>(request.ResourceClaims); - - var resolvedResourceClaims = strategyResolver.ResolveAuthStrategies(resourceClaims).ToList(); - - addOrEditResourcesOnClaimSetCommand.Execute(addedClaimSetId, resolvedResourceClaims); + }); - var claimSet = getClaimSetByIdQuery.Execute(addedClaimSetId); - - var model = mapper.Map(claimSet); - model.ApplicationsCount = getApplications.ExecuteCount(addedClaimSetId); - model.ResourceClaims = getResourcesByClaimSetIdQuery.AllResources(addedClaimSetId) - .Select(r => mapper.Map(r)).ToList(); + return Results.Created($"/claimSets/{addedClaimSetId}", null); + } + [SwaggerSchema(Title = "AddClaimSetRequest")] + public class AddClaimSetRequest + { + [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] + public string? Name { get; set; } + } - return AdminApiResponse.Created( - model, - "ClaimSet", - $"/claimsets/{addedClaimSetId}", - new JsonSerializerSettings() - { - Formatting = Formatting.Indented, - ContractResolver = new ShouldSerializeContractResolver(odsSecurityModelResolver) - } - ); - } - - [SwaggerSchema(Title = "AddClaimSetRequest")] - public class Request - { - [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] - public string? Name { get; set; } - - [SwaggerSchema(Description = FeatureConstants.ResourceClaimsDescription, Nullable = false)] - public List? ResourceClaims { get; set; } - } - - public class Validator : AbstractValidator - { - private readonly IGetAllClaimSetsQuery _getAllClaimSetsQuery; - - public Validator(IGetAllClaimSetsQuery getAllClaimSetsQuery, - IGetResourceClaimsAsFlatListQuery getResourceClaimsAsFlatListQuery, - IGetAllAuthorizationStrategiesQuery getAllAuthorizationStrategiesQuery, - IOdsSecurityModelVersionResolver resolver, - IMapper mapper) - { - _getAllClaimSetsQuery = getAllClaimSetsQuery; - - var resourceClaims = (Lookup)getResourceClaimsAsFlatListQuery.Execute() - .ToLookup(rc => rc.Name?.ToLower()); - - var authStrategyNames = getAllAuthorizationStrategiesQuery.Execute() - .Select(a => a.AuthStrategyName).ToList(); - - RuleFor(m => m.Name).NotEmpty() - .Must(BeAUniqueName) - .WithMessage(FeatureConstants.ClaimSetAlreadyExistsMessage); - - RuleFor(m => m.Name) - .MaximumLength(255) - .WithMessage(FeatureConstants.ClaimSetNameMaxLengthMessage); - - RuleFor(m => m).Custom((claimSet, context) => - { - var resourceClaimValidator = new ResourceClaimValidator(resolver); - - if (claimSet.ResourceClaims != null && claimSet.ResourceClaims.Any()) - { - foreach (var resourceClaim in claimSet.ResourceClaims) - { - resourceClaimValidator.Validate(resourceClaims, authStrategyNames, - resourceClaim, mapper.Map>(claimSet.ResourceClaims), context, claimSet.Name); - } - } - }); - } - - private bool BeAUniqueName(string? name) - { - return _getAllClaimSetsQuery.Execute().All(x => x.Name != name); - } - } + public class Validator : AbstractValidator + { + private readonly IGetAllClaimSetsQuery _getAllClaimSetsQuery; + + public Validator(IGetAllClaimSetsQuery getAllClaimSetsQuery) + { + _getAllClaimSetsQuery = getAllClaimSetsQuery; + + RuleFor(m => m.Name).NotEmpty() + .Must(BeAUniqueName) + .WithMessage(FeatureConstants.ClaimSetAlreadyExistsMessage); + + RuleFor(m => m.Name) + .MaximumLength(255) + .WithMessage(FeatureConstants.ClaimSetNameMaxLengthMessage); + } + + private bool BeAUniqueName(string? name) + { + return _getAllClaimSetsQuery.Execute().All(x => x.Name != name); + } + } } diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ClaimSetModel.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ClaimSetModel.cs index ff74545a0..7b60e9dd1 100644 --- a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ClaimSetModel.cs +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ClaimSetModel.cs @@ -1,121 +1,108 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using Swashbuckle.AspNetCore.Annotations; -using EdFi.Ods.AdminApi.Infrastructure.Documentation; -using EdFi.Ods.AdminApi.Infrastructure; - -namespace EdFi.Ods.AdminApi.Features.ClaimSets; - -[SwaggerSchema(Title = "ClaimSet")] -public class ClaimSetModel -{ - public int Id { get; set; } - public string? Name { get; set; } - public bool IsSystemReserved { get; set; } - public int ApplicationsCount { get; set; } -} - -[SwaggerSchema(Title = "ClaimSetWithResources")] -public class ClaimSetDetailsModel : ClaimSetModel -{ - public List ResourceClaims { get; set; } = new(); -} - -[SwaggerSchema(Title = "ResourceClaim")] -public class ResourceClaimModel -{ - public string? Name { get; set; } - public bool Create { get; set; } - public bool Read { get; set; } - public bool Update { get; set; } - public bool Delete { get; set; } - [SwaggerExclude(EdFiOdsSecurityModelCompatibility.ThreeThroughFive)] - public bool ReadChanges { get; set; } - public AuthorizationStrategiesModel?[] DefaultAuthStrategiesForCRUD { get; set; } - public AuthorizationStrategiesModel?[] AuthStrategyOverridesForCRUD { get; set; } - - [SwaggerSchema(Description = "Children are collection of ResourceClaim")] - public List Children { get; set; } - public ResourceClaimModel() - { - Children = new List(); - DefaultAuthStrategiesForCRUD = Array.Empty(); - AuthStrategyOverridesForCRUD = Array.Empty(); - } +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Features.Applications; +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Infrastructure.Documentation; using Swashbuckle.AspNetCore.Annotations; +using System.Linq; using System.Text.Json.Serialization; +namespace EdFi.Ods.AdminApi.Features.ClaimSets; + +[SwaggerSchema(Title = "ClaimSet")] +public class ClaimSetModel +{ + public int Id { get; set; } + public string? Name { get; set; } + [JsonPropertyName("_isSystemReserved")] + [SwaggerSchema(ReadOnly = true)] + public bool IsSystemReserved { get; set; } + [JsonPropertyName("_applications")] + [SwaggerSchema(ReadOnly = true)] + public List Applications { get; set; } = new(); } + +[SwaggerSchema(Title = "ClaimSetWithResources")] +public class ClaimSetDetailsModel : ClaimSetModel +{ + public List ResourceClaims { get; set; } = new(); +} + + +[SwaggerSchema(Title = "ClaimSetResourceClaim")] +public class ClaimSetResourceClaimModel +{ + [SwaggerSchema(ReadOnly = true)] + public int Id { get; set; } + public string? Name { get; set; } + public List? Actions { get; set; } + + [JsonPropertyName("_defaultAuthorizationStrategiesForCRUD")] + [SwaggerSchema(ReadOnly = true)] + public List DefaultAuthorizationStrategiesForCRUD { get; set; } -[SwaggerSchema(Title = "ResourceClaim")] -public class RequestResourceClaimModel -{ - public string? Name { get; set; } - public bool Create { get; set; } - public bool Read { get; set; } - public bool Update { get; set; } - public bool Delete { get; set; } - [SwaggerExclude(EdFiOdsSecurityModelCompatibility.ThreeThroughFive)] - public bool ReadChanges { get; set; } - public AuthorizationStrategiesModel?[] AuthStrategyOverridesForCRUD { get; set; } - - [SwaggerSchema(Description = "Children are collection of ResourceClaim")] - public List Children { get; set; } - public RequestResourceClaimModel() - { - Children = new List(); - AuthStrategyOverridesForCRUD = Array.Empty(); - } -} - -public class ChildrenRequestResourceClaimModel : RequestResourceClaimModel -{ - public List Children { get; set; } + public List AuthorizationStrategyOverridesForCRUD { get; set; } + + [SwaggerSchema(Description = "Children are collection of ResourceClaim")] + public List Children { get; set; } + + public ClaimSetResourceClaimModel() + { + Children = new List(); + DefaultAuthorizationStrategiesForCRUD = new List(); + AuthorizationStrategyOverridesForCRUD = new List(); + Actions = new List(); + } } -[SwaggerSchema(Title = "AuthorizationStrategies")] -public class AuthorizationStrategiesModel -{ - public AuthorizationStrategyModel?[] AuthorizationStrategies { get; set; } - public AuthorizationStrategiesModel() - { - AuthorizationStrategies = Array.Empty(); - - } -} - -[SwaggerSchema(Title = "AuthorizationStrategy")] -public class AuthorizationStrategyModel -{ - [SwaggerExclude] - public int AuthStrategyId { get; set; } - - public string? AuthStrategyName { get; set; } - - [SwaggerExclude] - public string? DisplayName { get; set; } - - public bool IsInheritedFromParent { get; set; } -} - -public class EditClaimSetModel : IEditClaimSetModel -{ - public string? ClaimSetName { get; set; } - - public int ClaimSetId { get; set; } -} - -public class UpdateResourcesOnClaimSetModel : IUpdateResourcesOnClaimSetModel -{ - public int ClaimSetId { get; set; } - - public List? ResourceClaims { get; set; } = new List(); -} - -public class DeleteClaimSetModel : IDeleteClaimSetModel -{ - public string? Name { get; set; } - - public int Id { get; set; } -} +public class ChildrenClaimSetResource : ClaimSetResourceClaimModel +{ + [SwaggerSchema(Description = "Children are collection of ResourceClaim")] + public new ClaimSetResourceClaimModel Children { get; set; } + + public ChildrenClaimSetResource() + { + Children = new ClaimSetResourceClaimModel(); + } +} +[SwaggerSchema(Title = "ResourceClaimModel")] +public class ResourceClaimModel +{ + public int Id { get; set; } + public string? Name { get; set; } + public int? ParentId { get; set; } + public string? ParentName { get; set; } + [SwaggerSchema(Description = "Children are collection of SimpleResourceClaimModel")] + public List Children { get; set; } + public ResourceClaimModel() + { + Children = new List(); + } +} + +public class EditClaimSetModel : IEditClaimSetModel +{ + public string? ClaimSetName { get; set; } + + public int ClaimSetId { get; set; } +} + +public class UpdateResourcesOnClaimSetModel : IUpdateResourcesOnClaimSetModel +{ + public int ClaimSetId { get; set; } + + public List? ResourceClaims { get; set; } = new List(); +} + +public class DeleteClaimSetModel : IDeleteClaimSetModel +{ + public string? Name { get; set; } + + public int Id { get; set; } +} +public interface IResourceClaimOnClaimSetRequest +{ + int ClaimSetId { get; } + int ResourceClaimId { get; } + public List? ResourceClaimActions { get; } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/CopyClaimSet.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/CopyClaimSet.cs new file mode 100644 index 000000000..6fae72ff7 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/CopyClaimSet.cs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApp.Management.ClaimSetEditor; +using FluentValidation; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.ClaimSets; + +public class CopyClaimSet : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPost(endpoints, "/claimSets/copy", Handle) + .WithSummary("Copies the existing claimset and create a new one.") + .WithRouteOptions(b => b.WithResponseCode(201)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task Handle(Validator validator, ICopyClaimSetCommand copyClaimSetCommand, + IGetClaimSetByIdQuery getClaimSetByIdQuery, + IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, + IGetApplicationsByClaimSetIdQuery getApplications, + IMapper mapper, + CopyClaimSetRequest request) + { + await validator.GuardAsync(request); + var copiedClaimSetId = copyClaimSetCommand.Execute(request); + + return Results.Created($"/claimSets/{copiedClaimSetId}", null); + } + + [SwaggerSchema(Title = "CopyClaimSetRequest")] + public class CopyClaimSetRequest : ICopyClaimSetModel + { + [SwaggerSchema(Description = FeatureConstants.ClaimSetIdToCopy, Nullable = false)] + public int OriginalId { get; set; } + + [SwaggerSchema(Description = "New claimset name", Nullable = false)] + public string? Name { get; set; } + } + + public class Validator : AbstractValidator + { + private readonly IGetAllClaimSetsQuery _getAllClaimSetsQuery; + private readonly IGetClaimSetByIdQuery _getClaimSetByIdQuery; + + public Validator(IGetAllClaimSetsQuery getAllClaimSetsQuery, + IGetClaimSetByIdQuery getClaimSetByIdQuery) + { + _getAllClaimSetsQuery = getAllClaimSetsQuery; + _getClaimSetByIdQuery = getClaimSetByIdQuery; + + RuleFor(m => m.OriginalId) + .Must(BeAnExistingClaimSet) + .WithMessage("No such claim set exists in the database.Please provide valid claimset id."); + + RuleFor(m => m.Name).NotEmpty() + .Must(BeAUniqueName) + .WithMessage(FeatureConstants.ClaimSetAlreadyExistsMessage); + + RuleFor(m => m.Name) + .MaximumLength(255) + .WithMessage(FeatureConstants.ClaimSetNameMaxLengthMessage); + } + + private bool BeAnExistingClaimSet(int id) + { + try + { + _getClaimSetByIdQuery.Execute(id); + return true; + } + catch (AdminApiException) + { + throw new NotFoundException("claimSet", id); + } + } + + private bool BeAUniqueName(string? name) + { + return _getAllClaimSetsQuery.Execute().All(x => !string.IsNullOrEmpty(x.Name) + && !x.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/DeleteClaimSet.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/DeleteClaimSet.cs index bf6e4d39a..7288939cd 100644 --- a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/DeleteClaimSet.cs +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/DeleteClaimSet.cs @@ -3,12 +3,16 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.AdminApi.Common.Features; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; using FluentValidation; using FluentValidation.Results; using Microsoft.AspNetCore.Mvc; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; namespace EdFi.Ods.AdminApi.Features.ClaimSets; @@ -16,10 +20,10 @@ public class DeleteClaimSet : IFeature { public void MapEndpoints(IEndpointRouteBuilder endpoints) { - AdminApiEndpointBuilder.MapDelete(endpoints, "/claimsets/{id}", Handle) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponseCode(200, FeatureConstants.DeletedSuccessResponseDescription)) - .BuildForVersions(AdminApiVersions.V1); + AdminApiEndpointBuilder.MapDelete(endpoints, "/claimSets/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureCommonConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V2); } public Task Handle(IDeleteClaimSetCommand deleteClaimSetCommand, [FromServices] IGetClaimSetByIdQuery getClaimSetByIdQuery, IGetApplicationsByClaimSetIdQuery getApplications, int id) @@ -36,18 +40,16 @@ public Task Handle(IDeleteClaimSetCommand deleteClaimSetCommand, [FromS throw new ValidationException(new[] { new ValidationFailure(nameof(id), exception.Message) }); } - return Task.FromResult(AdminApiResponse.Deleted("ClaimSet")); + return Task.FromResult(Results.Ok("ClaimSet".ToJsonObjectResponseDeleted())); } private static void CheckClaimSetExists(int id, IGetClaimSetByIdQuery query) { - try - { - query.Execute(id); - } - catch (AdminApiException) - { - throw new NotFoundException("claimset", id); + var claimSet = query.Execute(id); + if (claimSet != null && !claimSet.IsEditable) + { + throw new ValidationException(new[] { new ValidationFailure(nameof(id), + $"Claim set ({claimSet.Name}) is system reserved. May not be modified.") }); } } diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/EditClaimSet.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/EditClaimSet.cs index 85bc47143..8466bdcd6 100644 --- a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/EditClaimSet.cs +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/EditClaimSet.cs @@ -4,27 +4,27 @@ // See the LICENSE and NOTICES files in the project root for more information. using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using EdFi.Ods.AdminApi.Infrastructure.JsonContractResolvers; -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Infrastructure.Documentation; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; using FluentValidation; using FluentValidation.Results; -using Newtonsoft.Json; -using Swashbuckle.AspNetCore.Annotations; - +using Swashbuckle.AspNetCore.Annotations; + namespace EdFi.Ods.AdminApi.Features.ClaimSets; public class EditClaimSet : IFeature { public void MapEndpoints(IEndpointRouteBuilder endpoints) { - AdminApiEndpointBuilder.MapPut(endpoints, "/claimsets/{id}", Handle) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V1); + AdminApiEndpointBuilder.MapPut(endpoints, "/claimSets/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200)) + .BuildForVersions(AdminApiVersions.V2); } public async Task Handle(Validator validator, IEditClaimSetCommand editClaimSetCommand, @@ -32,10 +32,9 @@ public async Task Handle(Validator validator, IEditClaimSetCommand edit IGetClaimSetByIdQuery getClaimSetByIdQuery, IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, IGetApplicationsByClaimSetIdQuery getApplications, - IAuthStrategyResolver strategyResolver, - IOdsSecurityModelVersionResolver odsSecurityModelResolver, + IAuthStrategyResolver strategyResolver, IMapper mapper, - Request request, int id) + EditClaimSetRequest request, int id) { request.Id = id; await validator.GuardAsync(request); @@ -46,126 +45,72 @@ public async Task Handle(Validator validator, IEditClaimSetCommand edit ClaimSetId = id }; - int updatedClaimSetId; try { - updatedClaimSetId = editClaimSetCommand.Execute(editClaimSetModel); + editClaimSetCommand.Execute(editClaimSetModel); } catch (AdminApiException exception) { throw new ValidationException(new[] { new ValidationFailure(nameof(id), exception.Message) }); } - - var resourceClaims = mapper.Map>(request.ResourceClaims); - var resolvedResourceClaims = strategyResolver.ResolveAuthStrategies(resourceClaims).ToList(); - - updateResourcesOnClaimSetCommand.Execute( - new UpdateResourcesOnClaimSetModel { ClaimSetId = updatedClaimSetId, ResourceClaims = resolvedResourceClaims }); - - var claimSet = getClaimSetByIdQuery.Execute(updatedClaimSetId); - - var model = mapper.Map(claimSet); - model.ApplicationsCount = getApplications.ExecuteCount(updatedClaimSetId); - model.ResourceClaims = getResourcesByClaimSetIdQuery.AllResources(updatedClaimSetId) - .Select(r => mapper.Map(r)).ToList(); - - return AdminApiResponse.Updated( - model, - "ClaimSet", - new JsonSerializerSettings() - { - Formatting = Formatting.Indented, - ContractResolver = new ShouldSerializeContractResolver(odsSecurityModelResolver) - } - ); - } - - [SwaggerSchema(Title = "EditClaimSetRequest")] - public class Request - { - [SwaggerSchema(Description = "ClaimSet id", Nullable = false)] - public int Id { get; set; } - - [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] - public string? Name { get; set; } - - [SwaggerSchema(Description = FeatureConstants.ResourceClaimsDescription, Nullable = false)] - public List? ResourceClaims { get; set; } - } - - public class Validator : AbstractValidator - { - private readonly IGetClaimSetByIdQuery _getClaimSetByIdQuery; - private readonly IGetAllClaimSetsQuery _getAllClaimSetsQuery; - - public Validator(IGetClaimSetByIdQuery getClaimSetByIdQuery, - IGetAllClaimSetsQuery getAllClaimSetsQuery, - IGetResourceClaimsAsFlatListQuery getResourceClaimsAsFlatListQuery, - IGetAllAuthorizationStrategiesQuery getAllAuthorizationStrategiesQuery, - IOdsSecurityModelVersionResolver resolver, - IMapper mapper) - { - _getClaimSetByIdQuery = getClaimSetByIdQuery; - _getAllClaimSetsQuery = getAllClaimSetsQuery; - - var resourceClaims = (Lookup)getResourceClaimsAsFlatListQuery.Execute() - .ToLookup(rc => rc.Name?.ToLower()); - - var authStrategyNames = getAllAuthorizationStrategiesQuery.Execute() - .Select(a => a.AuthStrategyName).ToList(); - - RuleFor(m => m.Id).NotEmpty(); - - RuleFor(m => m.Id) - .Must(BeAnExistingClaimSet) - .WithMessage(FeatureConstants.ClaimSetNotFound); - - RuleFor(m => m.Name) - .NotEmpty() - .Must(BeAUniqueName) - .WithMessage(FeatureConstants.ClaimSetAlreadyExistsMessage) - .When(m => BeAnExistingClaimSet(m.Id) && NameIsChanged(m)); - - RuleFor(m => m.Name) - .MaximumLength(255) - .WithMessage(FeatureConstants.ClaimSetNameMaxLengthMessage); - - RuleFor(m => m).Custom((claimSet, context) => - { - var resourceClaimValidator = new ResourceClaimValidator(resolver); - - if (claimSet.ResourceClaims != null && claimSet.ResourceClaims.Any()) - { - foreach (var resourceClaim in claimSet.ResourceClaims) - { - resourceClaimValidator.Validate(resourceClaims, authStrategyNames, - resourceClaim, mapper.Map>(claimSet.ResourceClaims), context, claimSet.Name); - } - } - }); - } - - private bool BeAnExistingClaimSet(int id) - { - try - { - _getClaimSetByIdQuery.Execute(id); - return true; - } - catch (AdminApiException) - { - throw new NotFoundException("claimSet", id); - } - } - - private bool NameIsChanged(Request model) - { - return _getClaimSetByIdQuery.Execute(model.Id).Name != model.Name; - } - - private bool BeAUniqueName(string? name) - { - return _getAllClaimSetsQuery.Execute().All(x => x.Name != name); - } + return Results.Ok(); } + + [SwaggerSchema(Title = "EditClaimSetRequest")] + public class EditClaimSetRequest + { + [SwaggerExclude] + public int Id { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] + public string? Name { get; set; } + } + + public class Validator : AbstractValidator + { + private readonly IGetClaimSetByIdQuery _getClaimSetByIdQuery; + private readonly IGetAllClaimSetsQuery _getAllClaimSetsQuery; + + public Validator(IGetClaimSetByIdQuery getClaimSetByIdQuery, + IGetAllClaimSetsQuery getAllClaimSetsQuery) + { + _getClaimSetByIdQuery = getClaimSetByIdQuery; + _getAllClaimSetsQuery = getAllClaimSetsQuery; + + RuleFor(m => m.Id).NotEmpty(); + + RuleFor(m => m.Id) + .Must(BeAnExistingClaimSet) + .WithMessage(FeatureConstants.ClaimSetNotFound); + + RuleFor(m => m.Name) + .NotEmpty() + .Must(BeAUniqueName) + .WithMessage(FeatureConstants.ClaimSetAlreadyExistsMessage) + .When(m => BeAnExistingClaimSet(m.Id) && NameIsChanged(m)); + + RuleFor(m => m.Name) + .MaximumLength(255) + .WithMessage(FeatureConstants.ClaimSetNameMaxLengthMessage); + } + + private bool BeAnExistingClaimSet(int id) + { + _getClaimSetByIdQuery.Execute(id); + return true; + } + + private bool NameIsChanged(EditClaimSetRequest model) + { + return _getClaimSetByIdQuery.Execute(model.Id).Name != model.Name; + } + + private bool BeAUniqueName(string? name) + { + return _getAllClaimSetsQuery.Execute().All(x => x.Name != name); + } + } + + + } diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ExportClaimSet.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ExportClaimSet.cs new file mode 100644 index 000000000..7bd502702 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ExportClaimSet.cs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Features.Applications; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; + +namespace EdFi.Ods.AdminApi.Features.ClaimSets; + +public class ExportClaimSet : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/claimSets/{id}/export", GetClaimSet) + .WithSummary("Exports a specific claimset by id") + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static Task GetClaimSet(IGetClaimSetByIdQuery getClaimSetByIdQuery, + IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, + IGetApplicationsByClaimSetIdQuery getApplications, IMapper mapper, int id) + { + var claimSet = getClaimSetByIdQuery.Execute(id); + + var allResources = getResourcesByClaimSetIdQuery.AllResources(id); + var applications = getApplications.Execute(id); + var claimSetData = mapper.Map(claimSet); + if (applications != null) + { + claimSetData.Applications = mapper.Map>(applications); + } + if (allResources != null) + { + claimSetData.ResourceClaims = mapper.Map>(allResources.ToList()); + } + + return Task.FromResult(Results.Ok(claimSetData)); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ImportClaimSet.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ImportClaimSet.cs new file mode 100644 index 000000000..d556574f7 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ImportClaimSet.cs @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using FluentValidation; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.ClaimSets; + +public class ImportClaimSet : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPost(endpoints, "/claimSets/import", Handle) + .WithSummary("Imports a new claimset") + .WithRouteOptions(b => b.WithResponseCode(201)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal async Task Handle(Validator validator, AddClaimSetCommand addClaimSetCommand, + AddOrEditResourcesOnClaimSetCommand addOrEditResourcesOnClaimSetCommand, + IGetClaimSetByIdQuery getClaimSetByIdQuery, + IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, + IGetApplicationsByClaimSetIdQuery getApplications, + IAuthStrategyResolver strategyResolver, + IMapper mapper, + ImportClaimSetRequest request) + { + await validator.GuardAsync(request); + var addedClaimSetId = addClaimSetCommand.Execute(new AddClaimSetModel + { + ClaimSetName = request.Name ?? string.Empty + }); + + var resourceClaims = mapper.Map>(request.ResourceClaims); + var resolvedResourceClaims = strategyResolver.ResolveAuthStrategies(resourceClaims).ToList(); + + addOrEditResourcesOnClaimSetCommand.Execute(addedClaimSetId, resolvedResourceClaims); + + var claimSet = getClaimSetByIdQuery.Execute(addedClaimSetId); + + return Results.Created($"/claimSets/{claimSet.Id}", null); + } + + [SwaggerSchema(Title = "ImportClaimSetRequest")] + public class ImportClaimSetRequest + { + [SwaggerSchema(Description = FeatureConstants.ClaimSetNameDescription, Nullable = false)] + public string? Name { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ResourceClaimsDescription, Nullable = false)] + public List? ResourceClaims { get; set; } + } + + public class Validator : AbstractValidator + { + private readonly IGetAllClaimSetsQuery _getAllClaimSetsQuery; + + public Validator(IGetAllClaimSetsQuery getAllClaimSetsQuery, + IGetResourceClaimsAsFlatListQuery getResourceClaimsAsFlatListQuery, + IGetAllAuthorizationStrategiesQuery getAllAuthorizationStrategiesQuery, + IGetAllActionsQuery getAllActionsQuery, + IMapper mapper) + { + _getAllClaimSetsQuery = getAllClaimSetsQuery; + + var resourceClaims = (Lookup)getResourceClaimsAsFlatListQuery.Execute() + .ToLookup(rc => rc.Name?.ToLower()); + + var authStrategyNames = getAllAuthorizationStrategiesQuery.Execute() + .Select(a => a.AuthStrategyName).ToList(); + + RuleFor(m => m.Name).NotEmpty() + .Must(BeAUniqueName) + .WithMessage(FeatureConstants.ClaimSetAlreadyExistsMessage); + + RuleFor(m => m.Name) + .MaximumLength(255) + .WithMessage(FeatureConstants.ClaimSetNameMaxLengthMessage); + + RuleFor(m => m).Custom((claimSet, context) => + { + var resourceClaimValidator = new ResourceClaimValidator(); + var actions = getAllActionsQuery.Execute().Select(x => x.ActionName).ToList(); + + if (claimSet.ResourceClaims != null && claimSet.ResourceClaims.Any()) + { + foreach (var resourceClaim in claimSet.ResourceClaims) + { + resourceClaimValidator.Validate(resourceClaims, actions, authStrategyNames, + resourceClaim, mapper.Map>(claimSet.ResourceClaims), context, claimSet.Name); + } + } + }); + } + + private bool BeAUniqueName(string? name) + { + return _getAllClaimSetsQuery.Execute().All(x => x.Name != name); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ReadClaimSets.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ReadClaimSets.cs index c0aebc98c..a40008013 100644 --- a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ReadClaimSets.cs +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ReadClaimSets.cs @@ -4,46 +4,50 @@ // See the LICENSE and NOTICES files in the project root for more information. using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Features.Applications; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using EdFi.Ods.AdminApi.Infrastructure.JsonContractResolvers; -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; -using Newtonsoft.Json; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; namespace EdFi.Ods.AdminApi.Features.ClaimSets; public class ReadClaimSets : IFeature -{ +{ public void MapEndpoints(IEndpointRouteBuilder endpoints) { - AdminApiEndpointBuilder.MapGet(endpoints, "/claimsets", GetClaimSets) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponse>(200)) - .BuildForVersions(AdminApiVersions.V1); + AdminApiEndpointBuilder.MapGet(endpoints, "/claimSets", GetClaimSets) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse>(200)) + .BuildForVersions(AdminApiVersions.V2); - AdminApiEndpointBuilder.MapGet(endpoints, "/claimsets/{id}", GetClaimSet) - .WithDefaultDescription() + AdminApiEndpointBuilder.MapGet(endpoints, "/claimSets/{id}", GetClaimSet) + .WithDefaultSummaryAndDescription() .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V1); - } - - internal Task GetClaimSets( - IGetAllClaimSetsQuery getClaimSetsQuery, IGetApplicationsByClaimSetIdQuery getApplications, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static Task GetClaimSets( + IGetAllClaimSetsQuery getClaimSetsQuery, IGetApplicationsByClaimSetIdQuery getApplications, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams, int? id, string? name) { - var claimSets = getClaimSetsQuery.Execute(commonQueryParams).ToList(); - var model = mapper.Map>(claimSets); - foreach (var claimSet in model) + var claimSets = mapper.Map>(getClaimSetsQuery.Execute( + commonQueryParams, + id, + name)); + foreach (var claimSet in claimSets) { - claimSet.ApplicationsCount = getApplications.ExecuteCount(claimSet.Id); + claimSet.Applications = mapper.Map>(getApplications.Execute(claimSet.Id)); } - return Task.FromResult(AdminApiResponse>.Ok(model)); + return Task.FromResult(Results.Ok(claimSets)); } - - internal Task GetClaimSet(IGetClaimSetByIdQuery getClaimSetByIdQuery, + + internal static Task GetClaimSet(IGetClaimSetByIdQuery getClaimSetByIdQuery, IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, - IGetApplicationsByClaimSetIdQuery getApplications, - IOdsSecurityModelVersionResolver odsSecurityModelResolver, IMapper mapper, int id) + IGetApplicationsByClaimSetIdQuery getApplications, IMapper mapper, int id) { ClaimSet claimSet; try @@ -56,13 +60,17 @@ internal Task GetClaimSet(IGetClaimSetByIdQuery getClaimSetByIdQuery, } var allResources = getResourcesByClaimSetIdQuery.AllResources(id); - var claimSetData = mapper.Map(claimSet); - claimSetData.ApplicationsCount = getApplications.ExecuteCount(id); - claimSetData.ResourceClaims = mapper.Map>(allResources.ToList()); - return Task.FromResult(AdminApiResponse.Ok(claimSetData, - new JsonSerializerSettings() { - Formatting = Formatting.Indented, - ContractResolver = new ShouldSerializeContractResolver(odsSecurityModelResolver) - })); + var applications = getApplications.Execute(id); + var claimSetData = mapper.Map(claimSet); + if (applications != null) + { + claimSetData.Applications = mapper.Map>(applications); + } + if (allResources != null) + { + claimSetData.ResourceClaims = mapper.Map>(allResources.ToList()); + } + + return Task.FromResult(Results.Ok(claimSetData)); } } diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaimValidator.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaimValidator.cs index c42a1cc13..bc835a8a1 100644 --- a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaimValidator.cs +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaimValidator.cs @@ -3,86 +3,73 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System.Data; -using AutoMapper; -using EdFi.Ods.AdminApi.Infrastructure; -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using FluentValidation; - +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using FluentValidation; + namespace EdFi.Ods.AdminApi.Features.ClaimSets; public class ResourceClaimValidator { - private static List? _duplicateResources; - private readonly EdFiOdsSecurityModelCompatibility _odsApiVersion; - - public ResourceClaimValidator(IOdsSecurityModelVersionResolver resolver) - { - _duplicateResources = new List(); - _odsApiVersion = resolver.DetermineSecurityModel(); - } - - public void Validate(Lookup dbResourceClaims, - List dbAuthStrategies, RequestResourceClaimModel resourceClaim, List existingResourceClaims, + private static List? _duplicateResources = []; + + public void Validate(Lookup dbResourceClaims, List dbActions, + List dbAuthStrategies, ClaimSetResourceClaimModel resourceClaim, List existingResourceClaims, ValidationContext context, string? claimSetName) { context.MessageFormatter.AppendArgument("ClaimSetName", claimSetName); context.MessageFormatter.AppendArgument("ResourceClaimName", resourceClaim.Name); - var propertyName = "ResourceClaims"; - - if (existingResourceClaims.Count(x => x.Name == resourceClaim.Name) > 1) - { - if (_duplicateResources != null && resourceClaim.Name != null && !_duplicateResources.Contains(resourceClaim.Name)) - { - _duplicateResources.Add(resourceClaim.Name); - context.AddFailure(propertyName, "Only unique resource claims can be added. The following is a duplicate resource: '{ResourceClaimName}'"); - } - } + var propertyName = "ResourceClaims"; + ValidateDuplicateResourceClaim(resourceClaim, existingResourceClaims, context, propertyName); - if (!(resourceClaim.Create || resourceClaim.Delete || resourceClaim.Read || resourceClaim.Update || resourceClaim.ReadChanges)) - { - context.AddFailure(propertyName, "Only valid resources can be added. A resource must have at least one action associated with it to be added. The following is an invalid resource: '{ResourceClaimName}'"); - } + ValidateCRUD(resourceClaim.Actions, dbActions, context, propertyName); var resources = dbResourceClaims[resourceClaim.Name!.ToLower()].ToList(); + ValidateIfExist(context, propertyName, resources); + ValidateAuthStrategies(dbAuthStrategies, resourceClaim, context, propertyName); + ValidateAuthStrategiesOverride(dbAuthStrategies, resourceClaim, context, propertyName); + ValidateChildren(dbResourceClaims, dbActions, dbAuthStrategies, resourceClaim, context, claimSetName, propertyName, resources); + } + + public static void Validate(Lookup dbResourceClaims, List dbActions, IResourceClaimOnClaimSetRequest editResourceClaimOnClaimSetRequest, + ValidationContext context, string? claimSetName) + { + context.MessageFormatter.AppendArgument("ClaimSetName", claimSetName); + context.MessageFormatter.AppendArgument("ResourceClaimName", editResourceClaimOnClaimSetRequest.ResourceClaimId); + + var propertyName = "ResourceClaimActions"; + var resources = dbResourceClaims[editResourceClaimOnClaimSetRequest.ResourceClaimId].ToList(); + ValidateIfExist(context, propertyName, resources); + ValidateCRUD(editResourceClaimOnClaimSetRequest.ResourceClaimActions, dbActions, context, propertyName); + } + + private static void ValidateIfExist(ValidationContext context, string propertyName, List resources) + { if (!resources.Any()) { - context.AddFailure(propertyName, "This Claim Set contains a resource which is not in the system. Claimset Name: '{ClaimSetName}' Resource name: '{ResourceClaimName}'.\n"); - } - - if (resourceClaim.AuthStrategyOverridesForCRUD.Any()) + context.AddFailure(propertyName, "This Claim Set contains a resource which is not in the system. Claimset Name: '{ClaimSetName}' Resource: '{ResourceClaimName}'."); + } + } + + private static void ValidateDuplicateResourceClaim(ClaimSetResourceClaimModel resourceClaim, List existingResourceClaims, ValidationContext context, string propertyName) + { + if (existingResourceClaims.Count(x => x.Name == resourceClaim.Name) > 1) { - if (_odsApiVersion == EdFiOdsSecurityModelCompatibility.Six || _odsApiVersion == EdFiOdsSecurityModelCompatibility.FiveThreeCqe) + if (_duplicateResources == null || resourceClaim.Name == null || + _duplicateResources.Contains(resourceClaim.Name)) { - if (resourceClaim.AuthStrategyOverridesForCRUD.Count() < 5) - context.AddFailure(propertyName, "Please provide a list of 5 elements for 'AuthStrategyOverridesForCRUD' in the Resource name: '{ResourceClaimName}'"); + return; } - else if (_odsApiVersion == EdFiOdsSecurityModelCompatibility.ThreeThroughFive) - { - if (resourceClaim.AuthStrategyOverridesForCRUD.Count() < 4) - context.AddFailure(propertyName, "Please provide a list of 4 elements 'AuthStrategyOverridesForCRUD' in the Resource name: '{ResourceClaimName}'"); - } - else - { - foreach (var authStrategyOverride in resourceClaim.AuthStrategyOverridesForCRUD) - { - if (authStrategyOverride != null && authStrategyOverride.AuthorizationStrategies.Any()) - { - foreach (var authStrategy in authStrategyOverride.AuthorizationStrategies) - { - if (authStrategy?.AuthStrategyName != null && !dbAuthStrategies.Contains(authStrategy.AuthStrategyName)) - { - context.MessageFormatter.AppendArgument("AuthStrategyName", authStrategy.AuthStrategyName); - context.AddFailure(propertyName, "This resource claim contains an authorization strategy which is not in the system. Claimset Name: '{ClaimSetName}' Resource name: '{ResourceClaimName}' Authorization strategy: '{AuthStrategyName}'.\n"); - } - } - } - } - } - } - + _duplicateResources.Add(resourceClaim.Name); + context.AddFailure(propertyName, "Only unique resource claims can be added. The following is a duplicate resource: '{ResourceClaimName}'."); + } + } + + private void ValidateChildren(Lookup dbResourceClaims, List dbActions, + List dbAuthStrategies, ClaimSetResourceClaimModel resourceClaim, + ValidationContext context, string? claimSetName, string propertyName, List resources) + { if (resourceClaim.Children.Any()) { foreach (var child in resourceClaim.Children) @@ -101,12 +88,94 @@ public void Validate(Lookup dbResourceClaims, else if (!resources.Where(x => x is not null).Select(x => x.Id).Contains(childResource.ParentId)) { context.MessageFormatter.AppendArgument("CorrectParentResource", childResource.ParentName); - context.AddFailure(propertyName, "Child resource: '{ChildResource}' added to the wrong parent resource. Correct parent resource is: '{CorrectParentResource}'"); + context.AddFailure(propertyName, "Child resource: '{ChildResource}' added to the wrong parent resource. Correct parent resource is: '{CorrectParentResource}'."); } } } - Validate(dbResourceClaims, dbAuthStrategies, child, resourceClaim.Children, context, claimSetName); + Validate(dbResourceClaims, dbActions, dbAuthStrategies, child, resourceClaim.Children, context, claimSetName); + } + } + } + + private static void ValidateAuthStrategiesOverride(List dbAuthStrategies, + ClaimSetResourceClaimModel resourceClaim, ValidationContext context, string propertyName) + { + if (resourceClaim.AuthorizationStrategyOverridesForCRUD != null && resourceClaim.AuthorizationStrategyOverridesForCRUD.Any()) + { + foreach (var authStrategyOverrideWithAction in resourceClaim.AuthorizationStrategyOverridesForCRUD) + { + if (authStrategyOverrideWithAction?.AuthorizationStrategies != null) + { + foreach (var authStrategyOverride in authStrategyOverrideWithAction.AuthorizationStrategies) + { + if (authStrategyOverride?.AuthStrategyName != null && !dbAuthStrategies.Contains(authStrategyOverride.AuthStrategyName)) + { + context.MessageFormatter.AppendArgument("AuthStrategyName", authStrategyOverride.AuthStrategyName); + context.AddFailure(propertyName, "This resource claim contains an authorization strategy which is not in the system. Claimset Name: '{ClaimSetName}' Resource name: '{ResourceClaimName}' Authorization strategy: '{AuthStrategyName}'."); + } + } + } + } + } + } + + private static void ValidateAuthStrategies(List dbAuthStrategies, + ClaimSetResourceClaimModel resourceClaim, ValidationContext context, string propertyName) + { + if (resourceClaim.DefaultAuthorizationStrategiesForCRUD != null && resourceClaim.DefaultAuthorizationStrategiesForCRUD.Any()) + { + foreach (var defaultASWithAction in resourceClaim.DefaultAuthorizationStrategiesForCRUD) + { + if (defaultASWithAction?.AuthorizationStrategies == null) + { + continue; + } + + foreach (var defaultAS in defaultASWithAction.AuthorizationStrategies) + { + if (defaultAS?.AuthStrategyName != null && !dbAuthStrategies.Contains(defaultAS.AuthStrategyName)) + { + context.MessageFormatter.AppendArgument("AuthStrategyName", defaultAS.AuthStrategyName); + context.AddFailure(propertyName, "This resource claim contains an authorization strategy which is not in the system. Claimset Name: '{ClaimSetName}' Resource name: '{ResourceClaimName}' Authorization strategy: '{AuthStrategyName}'."); + } + } } - } - } + } + } + + private static void ValidateCRUD(List? resourceClaimActions, + List dbActions, ValidationContext context, string propertyName) + { + if (resourceClaimActions != null && resourceClaimActions.Any()) + { + var atleastAnActionEnabled = resourceClaimActions.Exists(x => x.Enabled); + if (!atleastAnActionEnabled) + { + context.AddFailure(propertyName, FeatureConstants.ResourceClaimOneActionNotSet); + } + else + { + var duplicates = resourceClaimActions.GroupBy(x => x.Name) + .Where(g => g.Count() > 1) + .Select(y => y.Key) + .ToList(); + foreach (var duplicate in duplicates) + { + context.AddFailure(propertyName, $"{duplicate} action is duplicated."); + } + foreach (var action in resourceClaimActions.Select(x => x.Name)) + { + if (!dbActions.Exists(actionName => actionName != null && + actionName.Equals(action, StringComparison.InvariantCultureIgnoreCase))) + { + context.AddFailure(propertyName, $"{action} is not a valid action."); + } + } + } + } + else + { + context.AddFailure(propertyName, $"Actions can not be empty."); + } + } } diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaims/DeleteResourceClaim.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaims/DeleteResourceClaim.cs new file mode 100644 index 000000000..b42dd0ee0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaims/DeleteResourceClaim.cs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using FluentValidation; +using FluentValidation.Results; +using EdFi.Ods.AdminApi.Common.Infrastructure; + +namespace EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims; + +public class DeleteResourceClaim : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapDelete(endpoints, "/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}", Handle) + .WithSummary("Deletes a resource claims association from a claimset") + .WithRouteOptions(b => b.WithResponseCode(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static async Task Handle(IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, + IGetClaimSetByIdQuery getClaimSetByIdQuery, + IAuthStrategyResolver strategyResolver, + IDeleteResouceClaimOnClaimSetCommand deleteResouceClaimOnClaimSetCommand, + IMapper mapper, int claimSetId, int resourceClaimId) + { + var claimSet = getClaimSetByIdQuery.Execute(claimSetId); + + if (!claimSet.IsEditable) + { + throw new ValidationException(new[] { new ValidationFailure(nameof(claimSetId), $"Claim set ({claimSet.Name}) is system reserved. May not be modified.") }); + } + + var resourceClaim = getResourcesByClaimSetIdQuery.SingleResource(claimSet.Id, resourceClaimId); + if (resourceClaim == null) + { + throw new NotFoundException("ResourceClaim", resourceClaimId); + } + else + { + deleteResouceClaimOnClaimSetCommand.Execute(claimSet.Id, resourceClaimId); + } + + return await Task.FromResult(Results.Ok()); + } + +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaims/EditAuthStrategy.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaims/EditAuthStrategy.cs new file mode 100644 index 000000000..7a4b389de --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaims/EditAuthStrategy.cs @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using FluentValidation; +using FluentValidation.Results; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims; + +public class EditAuthStrategy : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPost(endpoints, "/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/overrideAuthorizationStrategy", HandleOverrideAuthStrategies) + .WithSummaryAndDescription("Overrides the default authorization strategies on provided resource claim for a specific action.", "Override the default authorization strategies on provided resource claim for a specific action.\r\n\r\nex: actionName = read, authorizationStrategies= [ \"Ownershipbased\" ]") + .WithRouteOptions(b => b.WithResponseCode(200)) + .BuildForVersions(AdminApiVersions.V2); + + AdminApiEndpointBuilder.MapPost(endpoints, "/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/resetAuthorizationStrategies", HandleResetAuthStrategies) + .WithSummary("Resets to default authorization strategies on provided resource claim.") + .WithRouteOptions(b => b.WithResponseCode(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static async Task HandleOverrideAuthStrategies(OverrideAuthStategyOnClaimSetValidator validator, + OverrideDefaultAuthorizationStrategyCommand overrideDefaultAuthorizationStrategyCommand, IMapper mapper, + OverrideAuthStategyOnClaimSetRequest request, int claimSetId, int resourceClaimId) + { + request.ClaimSetId = claimSetId; + request.ResourceClaimId = resourceClaimId; + await validator.GuardAsync(request); + var model = mapper.Map(request); + overrideDefaultAuthorizationStrategyCommand.ExecuteOnSpecificAction(model); + return Results.Ok(); + } + + internal async Task HandleResetAuthStrategies(IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, + OverrideDefaultAuthorizationStrategyCommand overrideDefaultAuthorizationStrategyCommand, IGetClaimSetByIdQuery getClaimSetByIdQuery, + IMapper mapper, int claimSetId, int resourceClaimId) + { + var claimSet = getClaimSetByIdQuery.Execute(claimSetId); + + if (!claimSet.IsEditable) + { + throw new ValidationException(new[] { new ValidationFailure(nameof(claimSetId), $"Claim set ({claimSet.Name}) is system reserved. May not be modified.") }); + } + + var resourceClaims = getResourcesByClaimSetIdQuery.AllResources(claimSetId); + var allResourcesIds = new List(); + foreach (var resourceClaim in resourceClaims) + { + allResourcesIds.Add(resourceClaim.Id); + if (resourceClaim.Children != null && resourceClaim.Children.Any()) + { + foreach (var child in resourceClaim.Children) + { + allResourcesIds.Add(child.Id); + } + } + } + if (!allResourcesIds.Contains(resourceClaimId)) + { + throw new NotFoundException("ResourceClaim", resourceClaimId); + } + else + { + overrideDefaultAuthorizationStrategyCommand.ResetAuthorizationStrategyOverrides( + new OverrideAuthStrategyOnClaimSetModel() + { + ClaimSetId = claimSetId, + ResourceClaimId = resourceClaimId + }); + } + + return await Task.FromResult(Results.Ok()); + } + + + public class OverrideAuthStategyOnClaimSetValidator : AbstractValidator + { + public OverrideAuthStategyOnClaimSetValidator(IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, IGetAllAuthorizationStrategiesQuery getAllAuthorizationStrategiesQuery, IGetAllActionsQuery getAllActionsQuery, IGetClaimSetByIdQuery getClaimSetByIdQuery) + { + RuleFor(m => m.ClaimSetId).NotEqual(0); + RuleFor(m => m.ResourceClaimId).NotEqual(0); + RuleFor(m => m.ActionName).NotEmpty(); + RuleFor(m => m.AuthorizationStrategies).NotNull().NotEmpty(); + + RuleFor(m => m).Custom((overrideAuthStategyOnClaimSetRequest, context) => + { + + var resourceClaim = getResourcesByClaimSetIdQuery.SingleResource(overrideAuthStategyOnClaimSetRequest.ClaimSetId, overrideAuthStategyOnClaimSetRequest.ResourceClaimId); + if (resourceClaim == null) + { + context.AddFailure("ResourceClaim", "Resource claim doesn't exist for the Claim set provided"); + } + + var claimSet = getClaimSetByIdQuery.Execute(overrideAuthStategyOnClaimSetRequest.ClaimSetId); + if (!claimSet.IsEditable) + { + context.AddFailure("ClaimSetId", $"Claim set ({claimSet.Name}) is system reserved. May not be modified."); + } + + var authStrategies = getAllAuthorizationStrategiesQuery.Execute(); + foreach (var authStrategyName in overrideAuthStategyOnClaimSetRequest.AuthorizationStrategies!) + { + var validAuthStrategyName = authStrategies + .FirstOrDefault(a => a.AuthStrategyName!.ToLower() == authStrategyName!.ToLower()); + + if (validAuthStrategyName == null) + { + context.AddFailure("AuthorizationStrategies", $"{authStrategyName} doesn't exist."); + } + + } + + var actionName = getAllActionsQuery.Execute().AsEnumerable() + .FirstOrDefault(a => a.ActionName.ToLower() == overrideAuthStategyOnClaimSetRequest.ActionName!.ToLower()); + + if (actionName == null) + { + context.AddFailure("ActionName", "ActionName doesn't exist."); + } + }); + } + } + + + [SwaggerSchema(Title = "OverrideAuthStategyOnClaimSetRequest")] + public class OverrideAuthStategyOnClaimSetRequest : OverrideAuthStrategyOnClaimSetModel + { + [SwaggerSchema(Description = "AuthorizationStrategy Names", Nullable = false)] + public IEnumerable? AuthorizationStrategies { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaims/EditResourceClaimActions.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaims/EditResourceClaimActions.cs new file mode 100644 index 000000000..d7c2e6a92 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ResourceClaims/EditResourceClaimActions.cs @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Infrastructure.Documentation; +using FluentValidation; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims; + +public class EditResourceClaimActions : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapPost(endpoints, "/claimSets/{claimSetId}/resourceClaimActions", HandleAddResourceClaims) + .WithSummaryAndDescription("Adds ResourceClaimAction association to a claim set.", "Add resourceClaimAction association to claim set. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges.\r\nresouceclaimId is required fields.") + .WithRouteOptions(b => b.WithResponseCode(201)) + .BuildForVersions(AdminApiVersions.V2); + + AdminApiEndpointBuilder.MapPut(endpoints, "/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}", HandleEditResourceClaims) + .WithSummaryAndDescription("Updates the ResourceClaimActions to a specific resource claim on a claimset.", "Updates the resourceClaimActions to a specific resource claim on a claimset. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges.") + .WithRouteOptions(b => b.WithResponseCode(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static async Task HandleAddResourceClaims(ResourceClaimClaimSetValidator validator, + EditResourceOnClaimSetCommand editResourcesOnClaimSetCommand, + UpdateResourcesOnClaimSetCommand updateResourcesOnClaimSetCommand, + IGetClaimSetByIdQuery getClaimSetByIdQuery, + IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, + IMapper mapper, + AddResourceClaimOnClaimSetRequest request, int claimSetId) + { + request.ClaimSetId = claimSetId; + await validator.GuardAsync(request); + await ExecuteHandle(editResourcesOnClaimSetCommand, mapper, request); + return Results.Ok(); + } + + internal static async Task HandleEditResourceClaims(ResourceClaimClaimSetValidator validator, + EditResourceOnClaimSetCommand editResourcesOnClaimSetCommand, + UpdateResourcesOnClaimSetCommand updateResourcesOnClaimSetCommand, + IGetResourcesByClaimSetIdQuery getResourcesByClaimSetIdQuery, + IMapper mapper, + EditResourceClaimOnClaimSetRequest request, int claimSetId, int resourceClaimId) + { + request.ClaimSetId = claimSetId; + request.ResourceClaimId = resourceClaimId; + await validator.GuardAsync(request); + + await ExecuteHandle(editResourcesOnClaimSetCommand, mapper, request); + + return Results.Ok(); + } + + private static async Task ExecuteHandle(EditResourceOnClaimSetCommand editResourcesOnClaimSetCommand, IMapper mapper, IResourceClaimOnClaimSetRequest request) + { + var editResourceOnClaimSetModel = await Task.FromResult(mapper.Map(request)); + editResourceOnClaimSetModel.ResourceClaim!.Id = request.ResourceClaimId; + editResourcesOnClaimSetCommand.Execute(editResourceOnClaimSetModel); + } + + + [SwaggerSchema(Title = "AddResourceClaimActionsOnClaimSetRequest")] + public class AddResourceClaimOnClaimSetRequest : IResourceClaimOnClaimSetRequest + { + [SwaggerExclude] + public int ClaimSetId { get; set; } + + [SwaggerSchema(Description = "ResourceClaim id", Nullable = false)] + public int ResourceClaimId { get; set; } + + [SwaggerSchema(Nullable = false)] + public List? ResourceClaimActions { get; set; } = new List(); + } + + [SwaggerSchema(Title = "EditResourceClaimActionsOnClaimSetRequest")] + public class EditResourceClaimOnClaimSetRequest : IResourceClaimOnClaimSetRequest + { + [SwaggerExclude] + public int ClaimSetId { get; set; } + + [SwaggerExclude] + public int ResourceClaimId { get; set; } + + [SwaggerSchema(Nullable = false)] + public List ResourceClaimActions { get; set; } = new List(); + } + + public class ResourceClaimClaimSetValidator : AbstractValidator + { + public ResourceClaimClaimSetValidator(IGetClaimSetByIdQuery getClaimSetByIdQuery, + IGetResourceClaimsAsFlatListQuery getResourceClaimsAsFlatListQuery, + IGetAllActionsQuery getAllActionsQuery) + { + var resourceClaims = getResourceClaimsAsFlatListQuery.Execute(); + var resourceClaimsById = (Lookup)resourceClaims + .ToLookup(rc => rc.Id); + + RuleFor(m => m.ClaimSetId).NotEmpty(); + + RuleFor(m => m).Custom((resourceClaimOnClaimSetRequest, context) => + { + ClaimSet claimSet; + claimSet = getClaimSetByIdQuery.Execute(resourceClaimOnClaimSetRequest.ClaimSetId); + var actions = getAllActionsQuery.Execute().Select(x => x.ActionName).ToList(); + if (!claimSet.IsEditable) + { + context.AddFailure("ClaimSetId", $"Claim set ({claimSet.Name}) is system reserved. May not be modified."); + } + + if (resourceClaimOnClaimSetRequest.ResourceClaimActions != null) + { + ResourceClaimValidator.Validate(resourceClaimsById, actions, resourceClaimOnClaimSetRequest, context, claimSet!.Name); + } + else + { + context.AddFailure("ResourceClaimActions", FeatureConstants.InvalidResourceClaimActions); + } + }); + } + + } + + +} + + diff --git a/Application/EdFi.Ods.AdminApi/Features/Connect/ConnectController.cs b/Application/EdFi.Ods.AdminApi/Features/Connect/ConnectController.cs index c1ec60cbf..ab8ec40eb 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Connect/ConnectController.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Connect/ConnectController.cs @@ -3,7 +3,10 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Ods.AdminApi.Infrastructure.Security; +using System.Net; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure.Security; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; using FluentValidation; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authorization; @@ -14,24 +17,19 @@ namespace EdFi.Ods.AdminApi.Features.Connect; [AllowAnonymous] -[SwaggerResponse(400, FeatureConstants.BadRequestResponseDescription)] -[SwaggerResponse(500, FeatureConstants.InternalServerErrorResponseDescription)] -public class ConnectController : Controller +[SwaggerResponse(400, FeatureCommonConstants.BadRequestResponseDescription)] +[SwaggerResponse(500, FeatureCommonConstants.InternalServerErrorResponseDescription)] +[Route(SecurityConstants.ConnectRoute)] +public class ConnectController(ITokenService tokenService, IRegisterService registerService) : Controller { - private readonly ITokenService _tokenService; - private readonly IRegisterService _registerService; + private readonly ITokenService _tokenService = tokenService; + private readonly IRegisterService _registerService = registerService; - public ConnectController(ITokenService tokenService, IRegisterService registerService) - { - _tokenService = tokenService; - _registerService = registerService; - } - - [HttpPost(SecurityConstants.RegisterEndpoint)] + [HttpPost(SecurityConstants.RegisterActionName)] [Consumes("application/x-www-form-urlencoded"), Produces("application/json")] [SwaggerOperation("Registers new client", "Registers new client")] [SwaggerResponse(200, "Application registered successfully.")] - public async Task Register([FromForm] RegisterService.Request request) + public async Task Register([FromForm] RegisterService.RegisterClientRequest request) { if (await _registerService.Handle(request)) { @@ -40,15 +38,31 @@ public async Task Register([FromForm] RegisterService.Request req return new ForbidResult(); } - [HttpPost(SecurityConstants.TokenEndpoint)] + [HttpPost(SecurityConstants.TokenActionName)] [Consumes("application/x-www-form-urlencoded"), Produces("application/json")] [SwaggerOperation("Retrieves bearer token", "\nTo authenticate Swagger requests, execute using \"Authorize\" above, not \"Try It Out\" here.")] [SwaggerResponse(200, "Sign-in successful.")] + [SwaggerResponse(400, "Bad request, such as invalid scope.")] public async Task Token() { var request = HttpContext.GetOpenIddictServerRequest() ?? throw new ValidationException("Failed to parse token request"); - var principal = await _tokenService.Handle(request); - return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + try + { + var principal = await _tokenService.Handle(request); + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + catch (AdminApiException ex) when (ex.StatusCode == HttpStatusCode.BadRequest) + { + // Return a 400 Bad Request response for invalid scopes with proper content type + Response.ContentType = "application/problem+json"; + return BadRequest(new + { + error = "invalid_scope", + error_description = ex.Message, + status = 400, + title = "Invalid Scope" + }); + } } } diff --git a/Application/EdFi.Ods.AdminApi/Features/Connect/RegisterService.cs b/Application/EdFi.Ods.AdminApi/Features/Connect/RegisterService.cs index 5aed98c31..a6c82e907 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Connect/RegisterService.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Connect/RegisterService.cs @@ -3,7 +3,9 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Ods.AdminApi.Infrastructure.Security; +using System.Text.RegularExpressions; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Security; using FluentValidation; using FluentValidation.Results; using OpenIddict.Abstractions; @@ -13,23 +15,16 @@ namespace EdFi.Ods.AdminApi.Features.Connect; public interface IRegisterService { - Task Handle(RegisterService.Request request); + Task Handle(RegisterService.RegisterClientRequest request); } -public class RegisterService : IRegisterService +public partial class RegisterService(IConfiguration configuration, RegisterService.Validator validator, IOpenIddictApplicationManager applicationManager) : IRegisterService { - private readonly IConfiguration _configuration; - private readonly Validator _validator; - private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IConfiguration _configuration = configuration; + private readonly Validator _validator = validator; + private readonly IOpenIddictApplicationManager _applicationManager = applicationManager; - public RegisterService(IConfiguration configuration, Validator validator, IOpenIddictApplicationManager applicationManager) - { - _configuration = configuration; - _validator = validator; - _applicationManager = applicationManager; - } - - public async Task Handle(Request request) + public async Task Handle(RegisterClientRequest request) { if (!await RegistrationIsEnabledOrNecessary()) return false; @@ -37,9 +32,8 @@ public async Task Handle(Request request) await _validator.GuardAsync(request); var existingApp = await _applicationManager.FindByClientIdAsync(request.ClientId!); - if (existingApp != null) - throw new ValidationException(new[] { new ValidationFailure(nameof(request.ClientId), $"ClientId {request.ClientId} already exists") }); + throw new ValidationException([new ValidationFailure(nameof(request.ClientId), $"ClientId {request.ClientId} already exists")]); var application = new OpenIddictApplicationDescriptor { @@ -49,10 +43,13 @@ public async Task Handle(Request request) Permissions = { OpenIddictConstants.Permissions.Endpoints.Token, - OpenIddictConstants.Permissions.GrantTypes.ClientCredentials, - OpenIddictConstants.Permissions.Prefixes.Scope + SecurityConstants.Scopes.AdminApiFullAccess + OpenIddictConstants.Permissions.GrantTypes.ClientCredentials }, }; + foreach (var scopeValue in SecurityConstants.Scopes.AllScopes) + { + application.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scopeValue.Scope); + } await _applicationManager.CreateAsync(application); return true; @@ -60,23 +57,28 @@ public async Task Handle(Request request) private async Task RegistrationIsEnabledOrNecessary() { - var registrationIsEnabled = _configuration.GetValue("Authentication:AllowRegistration"); - var applicationCount = await _applicationManager.CountAsync(); - return registrationIsEnabled || applicationCount == 0; + var registrationIsEnabled = _configuration.GetValue("Authentication:AllowRegistration"); + return await Task.FromResult(registrationIsEnabled); } - public class Validator : AbstractValidator + public partial class Validator : AbstractValidator { - public Validator() - { - RuleFor(m => m.ClientId).NotEmpty(); - RuleFor(m => m.ClientSecret).NotEmpty(); - RuleFor(m => m.DisplayName).NotEmpty(); - } + public Validator() + { + RuleFor(m => m.ClientId).NotEmpty(); + RuleFor(m => m.ClientSecret) + .NotEmpty() + .Matches(ClientSecretValidatorRegex()) + .WithMessage(FeatureConstants.ClientSecretValidationMessage); + RuleFor(m => m.DisplayName).NotEmpty(); + } + + [GeneratedRegex(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z\d]).{32,128}$")] + private static partial Regex ClientSecretValidatorRegex(); } [SwaggerSchema(Title = "RegisterClientRequest")] - public class Request + public class RegisterClientRequest { [SwaggerSchema(Description = "Client id", Nullable = false)] public string? ClientId { get; set; } diff --git a/Application/EdFi.Ods.AdminApi/Features/Connect/TokenService.cs b/Application/EdFi.Ods.AdminApi/Features/Connect/TokenService.cs index 7252cd790..b37d692c5 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Connect/TokenService.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Connect/TokenService.cs @@ -3,8 +3,11 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using System.Net; using System.Security.Authentication; using System.Security.Claims; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Security; using Microsoft.AspNetCore.Authentication.JwtBearer; using OpenIddict.Abstractions; @@ -15,49 +18,50 @@ public interface ITokenService Task Handle(OpenIddictRequest request); } -public class TokenService : ITokenService +public class TokenService(IOpenIddictApplicationManager applicationManager) : ITokenService { - private readonly IOpenIddictApplicationManager _applicationManager; - - public TokenService(IOpenIddictApplicationManager applicationManager) - { - _applicationManager = applicationManager; - } + private readonly IOpenIddictApplicationManager _applicationManager = applicationManager; + private const string DENIED_AUTHENTICATION_MESSAGE = "Access Denied. Please review your information and try again."; public async Task Handle(OpenIddictRequest request) { if (!request.IsClientCredentialsGrantType()) { - throw new NotImplementedException("The specified grant type is not implemented"); + throw new NotImplementedException(DENIED_AUTHENTICATION_MESSAGE); } var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) ?? - throw new NotFoundException("Admin API Client", request.ClientId); + throw new NotFoundException("Access Denied", DENIED_AUTHENTICATION_MESSAGE); if (!await _applicationManager.ValidateClientSecretAsync(application, request.ClientSecret!)) { - throw new AuthenticationException("Invalid Admin API Client key and secret"); + throw new AuthenticationException(DENIED_AUTHENTICATION_MESSAGE); } - var requestedScopes = request.GetScopes(); - var appScopes = (await _applicationManager.GetPermissionsAsync(application)) - .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope)) - .Select(p => p[OpenIddictConstants.Permissions.Prefixes.Scope.Length..]) - .ToList(); - var missingScopes = requestedScopes.Where(s => !appScopes.Contains(s)).ToList(); - if (missingScopes.Any()) - throw new AuthenticationException($"Client is not allowed access to requested scope(s): {string.Join(", ", missingScopes)}"); + // Get all valid scopes from system definition + var allValidScopes = SecurityConstants.Scopes.AllScopes.Select(s => s.Scope).ToList(); - var displayName = await _applicationManager.GetDisplayNameAsync(application); + // Check if any of the requested scopes are not in the list of valid scopes + var validScopes = requestedScopes.Where(s => allValidScopes.Contains(s)).ToList(); + if (validScopes.Count == 0) + { + throw new AdminApiException("The request is missing required scope claims or has invalid scope values") + { + StatusCode = HttpStatusCode.BadRequest + }; + } + var displayName = await _applicationManager.GetDisplayNameAsync(application); var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme); identity.AddClaim(OpenIddictConstants.Claims.Subject, request.ClientId!, OpenIddictConstants.Destinations.AccessToken); identity.AddClaim(OpenIddictConstants.Claims.Name, displayName!, OpenIddictConstants.Destinations.AccessToken); - var principal = new ClaimsPrincipal(identity); principal.SetScopes(requestedScopes); - + foreach (var claim in principal.Claims) + { + claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); + } return principal; } } diff --git a/Application/EdFi.Ods.AdminApi/Features/FeatureConstants.cs b/Application/EdFi.Ods.AdminApi/Features/FeatureConstants.cs index 1825ba42c..4148a618c 100644 --- a/Application/EdFi.Ods.AdminApi/Features/FeatureConstants.cs +++ b/Application/EdFi.Ods.AdminApi/Features/FeatureConstants.cs @@ -5,7 +5,7 @@ namespace EdFi.Ods.AdminApi.Features; -public class FeatureConstants +public static class FeatureConstants { public const string VendorIdDescription = "Vendor/ company id"; public const string VendorNameDescription = "Vendor/ company name"; @@ -18,26 +18,54 @@ public class FeatureConstants public const string ClaimSetNameDescription = "Claim set name"; public const string ProfileIdDescription = "Profile id"; public const string EducationOrganizationIdsDescription = "Education organization ids"; + public const string Enable = "Indicates whether the ApiClient's credetials is enabled. Defaults to true if not provided."; + public const string ResourceClaimIdDescription = "Resource Claim Id"; + public const string ResourceClaimNameDescription = "Resource Claim Name"; public const string ResourceClaimsDescription = "Resource Claims"; public const string ApplicationNameLengthValidationMessage = "The Application Name {ApplicationName} would be too long for Admin App to set up necessary Application records." + " Consider shortening the name by {ExtraCharactersInName} character(s)."; public const string ClaimSetNameValidationMessage = "Please provide a valid claim set name."; public const string EdOrgIdsValidationMessage = "Please provide at least one education organization id."; public const string VendorIdValidationMessage = "Please provide valid vendor id."; - public const string DeletedSuccessResponseDescription = "Resource was successfully deleted."; - public const string InternalServerErrorResponseDescription = "Internal server error. An unhandled error occurred on the server. See the response body for details."; - public const string BadRequestResponseDescription = "Bad Request. The request was invalid and cannot be completed. See the response body for details."; public const string ClaimSetAlreadyExistsMessage = "A claim set with this name already exists in the database. Please enter a unique name."; + public const string TenantAlreadyExistsMessage = "A tenant with this name already exists. Please enter a unique name."; public const string ClaimSetNameMaxLengthMessage = "The claim set name must be less than 255 characters."; public const string ClaimSetNotFound = "No such claim set exists in the database."; - public const string OdsInstanceIdDescription = "Ods Instance id"; + public const string InvalidResourceClaimActions = "Please provide a valid resourceClaimActions object."; + public const string ResourceClaimOneActionNotSet = "A resource must have at least one action associated with it to be added."; + public const string OdsInstanceIdsDescription = "List of ODS instance id"; + public const string OdsInstanceIdsValidationMessage = "Please provide at least one ods instance id."; + public const string OdsInstanceIdValidationMessage = "Please provide valid ods instance id. The id {OdsInstanceId} does not exist."; + public const string ClaimSetIdToCopy = "ClaimSet id to copy"; + public const string ProfileName = "Profile name"; + public const string ProfileAlreadyExistsMessage = "A Profile with this name already exists in the database. Please enter a unique name."; + public const string AnotherProfileAlreadyExistsMessage = "Another Profile with this name already exists in the database. Please enter a unique name."; + public const string ProfileDefinition = "Profile definition"; public const string OdsInstanceName = "Ods Instance name"; public const string OdsInstanceInstanceType = "Ods Instance type"; - public const string OdsInstanceStatus = "Ods Instance status"; - public const string OdsInstanceIsExtended = "Ods Instance is extended"; - public const string OdsInstanceVersion = "Ods Instance version"; public const string OdsInstanceConnectionString = "Ods Instance connection string"; public const string OdsInstanceAlreadyExistsMessage = "An Ods instance with this name already exists in the database. Please enter a unique name."; public const string OdsInstanceCantBeDeletedMessage = "There are some {Table} associated to this OdsInstance. Can not be deleted."; + public const string OdsInstanceDerivativeCombinedKeyMustBeUnique = "The combined key ODS instance id and derivative type must be unique."; + public const string OdsInstanceContextCombinedKeyMustBeUnique = "The combined key ODS instance id and context key must be unique."; public const string OdsInstanceConnectionStringInvalid = "The connection string is not valid."; + public const string TenantConnectionStringInvalid = "The connection string is not valid."; + public const string OdsInstanceDerivativeIdDescription = "ODS instance derivative id."; + public const string OdsInstanceDerivativeOdsInstanceIdDescription = "ODS instance derivative ODS instance id."; + public const string OdsInstanceDerivativeDerivativeTypeDescription = "derivative type."; + public const string OdsInstanceDerivativeConnectionStringDescription = "connection string."; + public const string OdsInstanceDerivativeDerivativeTypeNotValid = "The value for the Derivative type is not allowed. The only accepted values are: 'ReadReplica' or 'Snapshot'."; + public const string OdsInstanceContextIdDescription = "ODS instance context id."; + public const string OdsInstanceContextOdsInstanceIdDescription = "ODS instance context ODS instance id."; + public const string OdsInstanceContextContextKeyDescription = "context key."; + public const string OdsInstanceContextContextValueDescription = "context value."; + public const string ClientSecretValidationMessage = "ClientSecret must contain at least one lowercase letter, one uppercase letter, one number, and one special character, and must be 32 to 128 characters long."; + public const string ActionIdDescription = "Action id"; + public const string ActionNameDescription = "Action name"; + + public const string ApiClientNameDescription = "Api client name"; + public const string ApiClientIsApprovedDescription = "Is approved"; + public const string ApiClientApplicationIdDescription = "Application id"; + public const string ApiClientNameLengthValidationMessage = "The Api Client Name {Name} would be too long for Admin App to set up necessary ApiClient records." + + " Consider shortening the name by {ExtraCharactersInName} character(s)."; } diff --git a/Application/EdFi.Ods.AdminApi/Features/FeatureToggleAttribute.cs b/Application/EdFi.Ods.AdminApi/Features/FeatureToggleAttribute.cs new file mode 100644 index 000000000..a70a3e17e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/FeatureToggleAttribute.cs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc; + +namespace EdFi.Ods.AdminApi.Features +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class FeatureToggleAttribute : ActionFilterAttribute, IFeatureToggleAttribute + { + private readonly string _featureName; + private readonly IConfiguration _configuration; + + public FeatureToggleAttribute(string featureName, IConfiguration configuration) + { + _featureName = featureName; + _configuration = configuration; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + var isDisabled = !_configuration.GetValue(_featureName); + if (isDisabled) + { + context.Result = new NotFoundResult(); + } + base.OnActionExecuting(context); + } + } +} + diff --git a/Application/EdFi.Ods.AdminApi/Features/IFeatureToggleAttribute.cs b/Application/EdFi.Ods.AdminApi/Features/IFeatureToggleAttribute.cs new file mode 100644 index 000000000..f31ba8c3b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/IFeatureToggleAttribute.cs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.AspNetCore.Mvc.Filters; + +namespace EdFi.Ods.AdminApi.Features +{ + public interface IFeatureToggleAttribute + { + void OnActionExecuting(ActionExecutingContext context); + } +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/Features/Information/ReadInformation.cs b/Application/EdFi.Ods.AdminApi/Features/Information/ReadInformation.cs index b964d962d..bd371d7be 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Information/ReadInformation.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Information/ReadInformation.cs @@ -3,8 +3,13 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Settings; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using Microsoft.Extensions.Options; using Swashbuckle.AspNetCore.Annotations; namespace EdFi.Ods.AdminApi.Features.Information; @@ -16,13 +21,22 @@ public void MapEndpoints(IEndpointRouteBuilder endpoints) endpoints.MapGet("", GetInformation) .WithMetadata(new SwaggerOperationAttribute("Retrieve API informational metadata", null)) .WithResponse(200) - .WithResponseCode(500, FeatureConstants.InternalServerErrorResponseDescription) + .WithResponseCode(500, FeatureCommonConstants.InternalServerErrorResponseDescription) .WithTags("Information") .AllowAnonymous(); } - internal InformationResult GetInformation() + internal static InformationResult GetInformation(IOptions options) { - return new InformationResult(ConstantsHelpers.Version, ConstantsHelpers.Build); + if (!Enum.TryParse(options.Value.AdminApiMode, true, out var adminApiMode)) + { + throw new InvalidOperationException($"Invalid adminApiMode: {options.Value.AdminApiMode}"); + } + return adminApiMode switch + { + AdminApiMode.V1 => new InformationResult(V1.Infrastructure.Helpers.ConstantsHelpers.Version, V1.Infrastructure.Helpers.ConstantsHelpers.Build), + AdminApiMode.V2 => new InformationResult(ConstantsHelpers.Version, ConstantsHelpers.Build), + _ => throw new InvalidOperationException($"Invalid adminApiMode: {adminApiMode}") + }; } } diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/AddOdsInstanceContext.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/AddOdsInstanceContext.cs new file mode 100644 index 000000000..dab366bfd --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/AddOdsInstanceContext.cs @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using FluentValidation; +using Swashbuckle.AspNetCore.Annotations; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +namespace EdFi.Ods.AdminApi.Features.OdsInstanceContext; + +public class AddOdsInstanceContext : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapPost(endpoints, "/odsInstanceContexts", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(201)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task Handle(Validator validator, IAddOdsInstanceContextCommand addOdsInstanceContextCommand, IMapper mapper, AddOdsInstanceContextRequest request) + { + await validator.GuardAsync(request); + var addedOdsInstanceContext = addOdsInstanceContextCommand.Execute(request); + return Results.Created($"/odsInstanceContexts/{addedOdsInstanceContext.OdsInstanceContextId}", null); + } + + + [SwaggerSchema(Title = "AddOdsInstanceContextRequest")] + public class AddOdsInstanceContextRequest : IAddOdsInstanceContextModel + { + [SwaggerSchema(Description = FeatureConstants.OdsInstanceContextOdsInstanceIdDescription, Nullable = false)] + public int OdsInstanceId { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceContextContextKeyDescription, Nullable = false)] + public string? ContextKey { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceContextContextValueDescription, Nullable = false)] + public string? ContextValue { get; set; } + } + + public class Validator : AbstractValidator + { + private readonly IGetOdsInstanceQuery _getOdsInstanceQuery; + private readonly IGetOdsInstanceContextsQuery _getOdsInstanceContextsQuery; + + public Validator(IGetOdsInstanceQuery getOdsInstanceQuery, IGetOdsInstanceContextsQuery getOdsInstanceContextsQuery) + { + _getOdsInstanceQuery = getOdsInstanceQuery; + _getOdsInstanceContextsQuery = getOdsInstanceContextsQuery; + RuleFor(m => m.ContextKey).NotEmpty(); + + RuleFor(m => m.ContextValue).NotEmpty(); + + RuleFor(m => m.OdsInstanceId) + .NotEqual(0) + .WithMessage(FeatureConstants.OdsInstanceIdValidationMessage); + + RuleFor(m => m.OdsInstanceId) + .Must(BeAnExistingOdsInstance) + .When(m => !m.OdsInstanceId.Equals(0)); + + RuleFor(odsContext => odsContext) + .Must(BeUniqueCombinedKey) + .WithMessage(FeatureConstants.OdsInstanceContextCombinedKeyMustBeUnique); + + } + + private bool BeAnExistingOdsInstance(int id) + { + _getOdsInstanceQuery.Execute(id); + return true; + } + + private bool BeUniqueCombinedKey(AddOdsInstanceContextRequest request) + { + return !_getOdsInstanceContextsQuery.Execute().Exists + (x => x.OdsInstance?.OdsInstanceId == request.OdsInstanceId && + x.ContextKey.Equals(request.ContextKey, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/DeleteOdsInstanceContext.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/DeleteOdsInstanceContext.cs new file mode 100644 index 000000000..e1f45432c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/DeleteOdsInstanceContext.cs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +namespace EdFi.Ods.AdminApi.Features.OdsInstanceContext; + +public class DeleteOdsInstanceContext : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapDelete(endpoints, "/odsInstanceContexts/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureCommonConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static Task Handle(IDeleteOdsInstanceContextCommand deleteOdsInstanceContextCommand, int id) + { + deleteOdsInstanceContextCommand.Execute(id); + return Task.FromResult(Results.Ok()); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/EditOdsInstanceContext.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/EditOdsInstanceContext.cs new file mode 100644 index 000000000..34a7d63cd --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/EditOdsInstanceContext.cs @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Infrastructure.Documentation; +using FluentValidation; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.OdsInstanceContext; + +public class EditOdsInstanceContext : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapPut(endpoints, "/odsInstanceContexts/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task Handle(Validator validator, IEditOdsInstanceContextCommand editOdsInstanceContextCommand, IMapper mapper, IUsersContext db, EditOdsInstanceContextRequest request, int id) + { + request.Id = id; + await validator.GuardAsync(request); + editOdsInstanceContextCommand.Execute(request); + return Results.Ok(); + } + + + [SwaggerSchema(Title = "EditOdsInstanceContextRequest")] + public class EditOdsInstanceContextRequest : IEditOdsInstanceContextModel + { + [SwaggerExclude] + [SwaggerSchema(Description = FeatureConstants.OdsInstanceContextIdDescription, Nullable = false)] + public int Id { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceContextOdsInstanceIdDescription, Nullable = false)] + public int OdsInstanceId { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceContextContextKeyDescription, Nullable = false)] + public string? ContextKey { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceContextContextValueDescription, Nullable = false)] + public string? ContextValue { get; set; } + } + + public class Validator : AbstractValidator + { + private readonly IGetOdsInstanceQuery _getOdsInstanceQuery; + private readonly IGetOdsInstanceContextsQuery _getOdsInstanceContextsQuery; + + public Validator(IGetOdsInstanceQuery getOdsInstanceQuery, IGetOdsInstanceContextsQuery getOdsInstanceContextsQuery) + { + _getOdsInstanceQuery = getOdsInstanceQuery; + _getOdsInstanceContextsQuery = getOdsInstanceContextsQuery; + + RuleFor(m => m.ContextKey).NotEmpty(); + RuleFor(m => m.ContextValue).NotEmpty(); + + RuleFor(m => m.OdsInstanceId) + .NotEqual(0) + .WithMessage(FeatureConstants.OdsInstanceIdValidationMessage); + + RuleFor(m => m.OdsInstanceId) + .Must(BeAnExistingOdsInstance) + .When(m => !m.OdsInstanceId.Equals(0)); + + RuleFor(odsContext => odsContext) + .Must(BeUniqueCombinedKey) + .WithMessage(FeatureConstants.OdsInstanceContextCombinedKeyMustBeUnique); + } + + private bool BeAnExistingOdsInstance(int id) + { + _getOdsInstanceQuery.Execute(id); + return true; + } + + private bool BeUniqueCombinedKey(EditOdsInstanceContextRequest request) + { + return !_getOdsInstanceContextsQuery.Execute().Exists + (x => x.OdsInstance?.OdsInstanceId == request.OdsInstanceId && + x.ContextKey.Equals(request.ContextKey, StringComparison.OrdinalIgnoreCase) && + x.OdsInstanceContextId != request.Id); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/OdsInstanceContextModel.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/OdsInstanceContextModel.cs new file mode 100644 index 000000000..cce5f727d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/OdsInstanceContextModel.cs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Swashbuckle.AspNetCore.Annotations; +using System.Text.Json.Serialization; + +namespace EdFi.Ods.AdminApi.Features.OdsInstanceContext; + +[SwaggerSchema(Title = "OdsInstanceContext")] +public class OdsInstanceContextModel +{ + [JsonPropertyName("id")] + public int OdsInstanceContextId { get; set; } + public int OdsInstanceId { get; set; } + public string? ContextKey { get; set; } + public string? ContextValue { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/ReadOdsInstanceContext.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/ReadOdsInstanceContext.cs new file mode 100644 index 000000000..bb430206d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceContext/ReadOdsInstanceContext.cs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +namespace EdFi.Ods.AdminApi.Features.OdsInstanceContext; + +public class ReadOdsInstanceContext : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstanceContexts", GetOdsInstanceContexts) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + + AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstanceContexts/{id}", GetOdsInstanceContext) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static Task GetOdsInstanceContexts(IGetOdsInstanceContextsQuery getOdsInstanceContextsQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams) + { + var odsInstanceContextList = mapper.Map>(getOdsInstanceContextsQuery.Execute(commonQueryParams)); + return Task.FromResult(Results.Ok(odsInstanceContextList)); + } + + internal static Task GetOdsInstanceContext(IGetOdsInstanceContextByIdQuery getOdsInstanceContextByIdQuery, IMapper mapper, int id) + { + var odsInstanceContext = getOdsInstanceContextByIdQuery.Execute(id); + var model = mapper.Map(odsInstanceContext); + return Task.FromResult(Results.Ok(model)); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/AddOdsInstanceDerivative.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/AddOdsInstanceDerivative.cs new file mode 100644 index 000000000..a6c9f63b2 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/AddOdsInstanceDerivative.cs @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using FluentValidation; +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.Annotations; +namespace EdFi.Ods.AdminApi.Features.OdsInstanceDerivative; + +public class AddOdsInstanceDerivative : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapPost(endpoints, "/odsInstanceDerivatives", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(201)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task Handle(Validator validator, IAddOdsInstanceDerivativeCommand addOdsInstanceDerivativeCommand, IMapper mapper, AddOdsInstanceDerivativeRequest request) + { + await validator.GuardAsync(request); + var addedOdsInstanceDerivative = addOdsInstanceDerivativeCommand.Execute(request); + return Results.Created($"/odsInstanceDerivatives/{addedOdsInstanceDerivative.OdsInstanceDerivativeId}", null); + } + + [SwaggerSchema(Title = "AddOdsInstanceDerivativeRequest")] + public class AddOdsInstanceDerivativeRequest : IAddOdsInstanceDerivativeModel + { + [SwaggerSchema(Description = FeatureConstants.OdsInstanceDerivativeOdsInstanceIdDescription, Nullable = false)] + public int OdsInstanceId { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceDerivativeDerivativeTypeDescription, Nullable = false)] + public string? DerivativeType { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceDerivativeConnectionStringDescription, Nullable = false)] + public string? ConnectionString { get; set; } + } + + public class Validator : AbstractValidator + { + private readonly IGetOdsInstanceQuery _getOdsInstanceQuery; + private readonly IGetOdsInstanceDerivativesQuery _getOdsInstanceDerivativesQuery; + private readonly string _databaseEngine; + + public Validator(IGetOdsInstanceQuery getOdsInstanceQuery, IGetOdsInstanceDerivativesQuery getOdsInstanceDerivativesQuery, IOptions options) + { + _getOdsInstanceQuery = getOdsInstanceQuery; + _getOdsInstanceDerivativesQuery = getOdsInstanceDerivativesQuery; + _databaseEngine = options.Value.DatabaseEngine ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); + + RuleFor(m => m.DerivativeType).NotEmpty(); + + RuleFor(m => m.DerivativeType) + .Matches("^(?i)(readreplica|snapshot)$") + .WithMessage(FeatureConstants.OdsInstanceDerivativeDerivativeTypeNotValid) + .When(m => !string.IsNullOrEmpty(m.DerivativeType)); + + RuleFor(m => m.OdsInstanceId) + .NotEqual(0) + .WithMessage(FeatureConstants.OdsInstanceIdValidationMessage); + + RuleFor(m => m.OdsInstanceId) + .Must(BeAnExistingOdsInstance) + .When(m => !m.OdsInstanceId.Equals(0)); + + RuleFor(m => m.ConnectionString) + .NotEmpty(); + + RuleFor(m => m.ConnectionString) + .Must(BeAValidConnectionString) + .WithMessage(FeatureConstants.OdsInstanceConnectionStringInvalid) + .When(m => !string.IsNullOrEmpty(m.ConnectionString)); + + RuleFor(odsDerivative => odsDerivative) + .Must(BeUniqueCombinedKey) + .WithMessage(FeatureConstants.OdsInstanceDerivativeCombinedKeyMustBeUnique); + } + + private bool BeAnExistingOdsInstance(int id) + { + _getOdsInstanceQuery.Execute(id); + return true; + } + + private bool BeAValidConnectionString(string? connectionString) + { + return ConnectionStringHelper.ValidateConnectionString(_databaseEngine, connectionString); + } + + private bool BeUniqueCombinedKey(AddOdsInstanceDerivativeRequest request) + { + return !_getOdsInstanceDerivativesQuery.Execute().Exists(x => x.OdsInstance?.OdsInstanceId == request.OdsInstanceId && x.DerivativeType.Equals(request.DerivativeType, StringComparison.OrdinalIgnoreCase)); + } + + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/DeleteOdsInstanceDerivative.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/DeleteOdsInstanceDerivative.cs new file mode 100644 index 000000000..62f43c1c4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/DeleteOdsInstanceDerivative.cs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +namespace EdFi.Ods.AdminApi.Features.OdsInstanceDerivative; + +public class DeleteOdsInstanceDerivative : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapDelete(endpoints, "/odsInstanceDerivatives/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureCommonConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static Task Handle(IDeleteOdsInstanceDerivativeCommand deleteOdsInstanceDerivativeCommand, int id) + { + deleteOdsInstanceDerivativeCommand.Execute(id); + return Task.FromResult(Results.Ok()); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/EditOdsInstanceDerivative.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/EditOdsInstanceDerivative.cs new file mode 100644 index 000000000..38b6f21b2 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/EditOdsInstanceDerivative.cs @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Infrastructure.Documentation; +using FluentValidation; +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.Annotations; +namespace EdFi.Ods.AdminApi.Features.OdsInstanceDerivative; + +public class EditOdsInstanceDerivative : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapPut(endpoints, "/odsInstanceDerivatives/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task Handle(Validator validator, IEditOdsInstanceDerivativeCommand editOdsInstanceDerivativeCommand, IMapper mapper, IUsersContext db, EditOdsInstanceDerivativeRequest request, int id) + { + request.Id = id; + SetCurrentConnectionString(db, request, id); + await validator.GuardAsync(request); + editOdsInstanceDerivativeCommand.Execute(request); + return Results.Ok(); + } + + private static void SetCurrentConnectionString(IUsersContext db, EditOdsInstanceDerivativeRequest request, int id) + { + if (string.IsNullOrEmpty(request.ConnectionString)) + request.ConnectionString = db.OdsInstanceDerivatives.Find(id)?.ConnectionString; + } + + [SwaggerSchema(Title = "EditOdsInstanceDerivativeRequest")] + public class EditOdsInstanceDerivativeRequest : IEditOdsInstanceDerivativeModel + { + [SwaggerExclude] + [SwaggerSchema(Description = FeatureConstants.OdsInstanceDerivativeIdDescription, Nullable = false)] + public int Id { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceDerivativeOdsInstanceIdDescription, Nullable = false)] + public int OdsInstanceId { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceDerivativeDerivativeTypeDescription, Nullable = false)] + public string? DerivativeType { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceDerivativeConnectionStringDescription, Nullable = false)] + public string? ConnectionString { get; set; } + } + + public class Validator : AbstractValidator + { + private readonly IGetOdsInstanceQuery _getOdsInstanceQuery; + private readonly IGetOdsInstanceDerivativesQuery _getOdsInstanceDerivativesQuery; + private readonly string _databaseEngine; + public Validator(IGetOdsInstanceQuery getOdsInstanceQuery, IGetOdsInstanceDerivativesQuery getOdsInstanceDerivativesQuery, IOptions options) + { + _getOdsInstanceQuery = getOdsInstanceQuery; + _getOdsInstanceDerivativesQuery = getOdsInstanceDerivativesQuery; + _databaseEngine = options.Value.DatabaseEngine ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); + + RuleFor(m => m.DerivativeType).NotEmpty(); + + RuleFor(m => m.DerivativeType) + .Matches("^(?i)(readreplica|snapshot)$") + .WithMessage(FeatureConstants.OdsInstanceDerivativeDerivativeTypeNotValid) + .When(m => !string.IsNullOrEmpty(m.DerivativeType)); + + RuleFor(m => m.OdsInstanceId) + .NotEqual(0) + .WithMessage(FeatureConstants.OdsInstanceIdValidationMessage); + + RuleFor(m => m.OdsInstanceId) + .Must(BeAnExistingOdsInstance) + .When(m => !m.OdsInstanceId.Equals(0)); + + RuleFor(m => m.ConnectionString) + .Must(BeAValidConnectionString) + .WithMessage(FeatureConstants.OdsInstanceConnectionStringInvalid) + .When(m => !string.IsNullOrWhiteSpace(m.ConnectionString)); + + RuleFor(odsDerivative => odsDerivative) + .Must(BeUniqueCombinedKey) + .WithMessage(FeatureConstants.OdsInstanceDerivativeCombinedKeyMustBeUnique); + } + + private bool BeAnExistingOdsInstance(int id) + { + _getOdsInstanceQuery.Execute(id); + return true; + } + + private bool BeAValidConnectionString(string? connectionString) + { + return ConnectionStringHelper.ValidateConnectionString(_databaseEngine, connectionString); + } + + private bool BeUniqueCombinedKey(EditOdsInstanceDerivativeRequest request) + { + return !_getOdsInstanceDerivativesQuery.Execute().Exists + (x => + x.OdsInstance?.OdsInstanceId == request.OdsInstanceId && + x.DerivativeType.Equals(request.DerivativeType, StringComparison.OrdinalIgnoreCase) && + x.OdsInstanceDerivativeId != request.Id); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/OdsInstanceDerivativeModel.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/OdsInstanceDerivativeModel.cs new file mode 100644 index 000000000..8ea872d95 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/OdsInstanceDerivativeModel.cs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.OdsInstanceDerivative; + +[SwaggerSchema(Title = "OdsInstanceDerivative")] +public class OdsInstanceDerivativeModel +{ + public int Id { get; set; } + public int? OdsInstanceId { get; set; } + public string? DerivativeType { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/ReadOdsInstanceDerivative.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/ReadOdsInstanceDerivative.cs new file mode 100644 index 000000000..2cb6da8bf --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstanceDerivative/ReadOdsInstanceDerivative.cs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +namespace EdFi.Ods.AdminApi.Features.OdsInstanceDerivative; + +public class ReadOdsInstanceDerivative : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstanceDerivatives", GetOdsInstanceDerivatives) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + + AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstanceDerivatives/{id}", GetOdsInstanceDerivative) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static Task GetOdsInstanceDerivatives(IGetOdsInstanceDerivativesQuery getOdsInstanceDerivativesQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams) + { + var odsInstanceDerivativeList = mapper.Map>(getOdsInstanceDerivativesQuery.Execute(commonQueryParams)); + return Task.FromResult(Results.Ok(odsInstanceDerivativeList)); + } + + internal static Task GetOdsInstanceDerivative(IGetOdsInstanceDerivativeByIdQuery getOdsInstanceDerivativeByIdQuery, IMapper mapper, int id) + { + var odsInstanceDerivative = getOdsInstanceDerivativeByIdQuery.Execute(id); + var model = mapper.Map(odsInstanceDerivative); + return Task.FromResult(Results.Ok(model)); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/AddOdsInstance.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/AddOdsInstance.cs index 67d1dad1b..8f1ef39dc 100644 --- a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/AddOdsInstance.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/AddOdsInstance.cs @@ -1,92 +1,96 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using AutoMapper; -using EdFi.Ods.AdminApi.Infrastructure; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using FluentValidation; -using Swashbuckle.AspNetCore.Annotations; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; - -namespace EdFi.Ods.AdminApi.Features.OdsInstances; - -public class AddOdsInstance : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder.MapPost(endpoints, "/odsInstances", Handle) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponseCode(201)) - .BuildForVersions(AdminApiVersions.V1); - } - - public async Task Handle(Validator validator, IAddOdsInstanceCommand addOdsInstanceCommand, IMapper mapper, AddOdsInstanceRequest request) - { - await validator.GuardAsync(request); - var addedOdsInstance = addOdsInstanceCommand.Execute(request); - var model = mapper.Map(addedOdsInstance); - return AdminApiResponse.Created(model, "odsInstance", $"/odsInstances/{model.OdsInstanceId}"); - } - - [SwaggerSchema(Title = "AddOdsInstanceRequest")] - public class AddOdsInstanceRequest : IAddOdsInstanceModel - { - [SwaggerSchema(Description = FeatureConstants.OdsInstanceName, Nullable = false)] - public string? Name { get; set; } - [SwaggerSchema(Description = FeatureConstants.OdsInstanceInstanceType, Nullable = true)] - public string? InstanceType { get; set; } - [SwaggerSchema(Description = FeatureConstants.OdsInstanceStatus, Nullable = true)] - public string? Status { get; set; } - [SwaggerSchema(Description = FeatureConstants.OdsInstanceIsExtended, Nullable = true)] - public bool? IsExtended { get; set; } - [SwaggerSchema(Description = FeatureConstants.OdsInstanceVersion, Nullable = true)] - public string? Version { get; set; } - } - - public class Validator : AbstractValidator - { - private readonly IGetOdsInstancesQuery _getOdsInstancesQuery; - private readonly string _databaseEngine; - public Validator(IGetOdsInstancesQuery getOdsInstancesQuery) - { - _getOdsInstancesQuery = getOdsInstancesQuery; - - RuleFor(m => m.Name) - .NotEmpty() - .Must(BeAUniqueName) - .WithMessage(FeatureConstants.OdsInstanceAlreadyExistsMessage); - - RuleFor(m => m.Name) - .MaximumLength(100) - .When(m => !string.IsNullOrEmpty(m.Name)); - - RuleFor(m => m.InstanceType) - .NotEmpty(); - - RuleFor(m => m.InstanceType) - .MaximumLength(100) - .When(m => !string.IsNullOrEmpty(m.InstanceType)); - - RuleFor(m => m.Status) - .NotEmpty(); - - RuleFor(m => m.Status) - .MaximumLength(100) - .When(m => !string.IsNullOrEmpty(m.Status)); - - RuleFor(m => m.Version) - .NotEmpty(); - - RuleFor(m => m.Version) - .MaximumLength(20) - .When(m => !string.IsNullOrEmpty(m.Version)); - } - - private bool BeAUniqueName(string? name) - { - return _getOdsInstancesQuery.Execute().TrueForAll(x => x.Name != name); - } - } -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Infrastructure.Providers.Interfaces; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using FluentValidation; +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.OdsInstances; + +public class AddOdsInstance : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapPost(endpoints, "/odsInstances", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(201)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task Handle( + Validator validator, + IAddOdsInstanceCommand addOdsInstanceCommand, + IMapper mapper, + ISymmetricStringEncryptionProvider encryptionProvider, + IOptions options, + AddOdsInstanceRequest request) + { + await validator.GuardAsync(request); + string encryptionKey = options.Value.EncryptionKey ?? throw new InvalidOperationException("EncryptionKey can't be null."); + request.ConnectionString = encryptionProvider.Encrypt(request.ConnectionString, Convert.FromBase64String(encryptionKey)); + var addedProfile = addOdsInstanceCommand.Execute(request); + return Results.Created($"/odsInstances/{addedProfile.OdsInstanceId}", null); + } + + [SwaggerSchema(Title = "AddOdsInstanceRequest")] + public class AddOdsInstanceRequest : IAddOdsInstanceModel + { + [SwaggerSchema(Description = FeatureConstants.OdsInstanceName, Nullable = false)] + public string? Name { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceInstanceType, Nullable = true)] + public string? InstanceType { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceConnectionString, Nullable = false)] + public string? ConnectionString { get; set; } + + } + + public class Validator : AbstractValidator + { + private readonly IGetOdsInstancesQuery _getOdsInstancesQuery; + private readonly string _databaseEngine; + public Validator(IGetOdsInstancesQuery getOdsInstancesQuery, IOptions options) + { + _getOdsInstancesQuery = getOdsInstancesQuery; + _databaseEngine = options.Value.DatabaseEngine ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); + + RuleFor(m => m.Name) + .NotEmpty() + .Must(BeAUniqueName) + .WithMessage(FeatureConstants.OdsInstanceAlreadyExistsMessage); + + RuleFor(m => m.InstanceType) + .MaximumLength(100) + .When(m => !string.IsNullOrEmpty(m.InstanceType)); + + RuleFor(m => m.ConnectionString) + .NotEmpty(); + + RuleFor(m => m.ConnectionString) + .Must(BeAValidConnectionString) + .WithMessage(FeatureConstants.OdsInstanceConnectionStringInvalid) + .When(m => !string.IsNullOrEmpty(m.ConnectionString)); + } + + private bool BeAUniqueName(string? name) + { + return _getOdsInstancesQuery.Execute().TrueForAll(x => x.Name != name); + } + + private bool BeAValidConnectionString(string? connectionString) + { + return ConnectionStringHelper.ValidateConnectionString(_databaseEngine, connectionString); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/DeleteOdsInstance.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/DeleteOdsInstance.cs index 17b7809f0..3ad80ebe7 100644 --- a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/DeleteOdsInstance.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/DeleteOdsInstance.cs @@ -4,10 +4,11 @@ // See the LICENSE and NOTICES files in the project root for more information. using EdFi.Admin.DataAccess.Models; -using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using EdFi.Ods.AdminApi.Infrastructure.Extensions; using FluentValidation; namespace EdFi.Ods.AdminApi.Features.OdsInstances; @@ -17,9 +18,9 @@ public class DeleteOdsInstance : IFeature public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder.MapDelete(endpoints, "/odsInstances/{id}", Handle) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponseCode(200, FeatureConstants.DeletedSuccessResponseDescription)) - .BuildForVersions(AdminApiVersions.V1); + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureCommonConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V2); } internal async Task Handle(IDeleteOdsInstanceCommand deleteOdsInstanceCommand, Validator validator, int id) @@ -27,7 +28,7 @@ internal async Task Handle(IDeleteOdsInstanceCommand deleteOdsInstanceC var request = new Request() { Id = id }; await validator.GuardAsync(request); deleteOdsInstanceCommand.Execute(request.Id); - return await Task.FromResult(AdminApiResponse.Deleted("odsInstance")); + return await Task.FromResult(Results.Ok("Ods Instance".ToJsonObjectResponseDeleted())); } public class Validator : AbstractValidator @@ -44,6 +45,14 @@ public Validator(IGetOdsInstanceQuery getOdsInstanceQuery, IGetApplicationsByOds .Must(NotHaveApplicationsRelationships) .WithMessage(FeatureConstants.OdsInstanceCantBeDeletedMessage) .When(Exist); + RuleFor(m => m.Id) + .Must(NotHaveOdsInstanceContextsRelationships) + .WithMessage(FeatureConstants.OdsInstanceCantBeDeletedMessage) + .When(Exist); + RuleFor(m => m.Id) + .Must(NotHaveOdsInstanceDerivativesRelationships) + .WithMessage(FeatureConstants.OdsInstanceCantBeDeletedMessage) + .When(Exist); } private bool Exist(Request request) @@ -54,9 +63,21 @@ private bool Exist(Request request) private bool NotHaveApplicationsRelationships(Request model, int odsIntanceId, ValidationContext context) { context.MessageFormatter.AppendArgument("Table", "Applications"); - List appList = _getApplicationByOdsInstanceIdQuery.Execute(odsIntanceId) ?? new List(); + List appList = _getApplicationByOdsInstanceIdQuery.Execute(odsIntanceId) ?? []; return appList.Count == 0; } + + private bool NotHaveOdsInstanceContextsRelationships(Request model, int odsIntanceId, ValidationContext context) + { + context.MessageFormatter.AppendArgument("Table", "OdsInstanceContexts"); + return OdsInstanceEntity!.OdsInstanceContexts.Count == 0; + } + + private bool NotHaveOdsInstanceDerivativesRelationships(Request model, int odsIntanceId, ValidationContext context) + { + context.MessageFormatter.AppendArgument("Table", "OdsInstanceDerivatives"); + return OdsInstanceEntity!.OdsInstanceDerivatives.Count == 0; + } } public class Request diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/EditOdsInstance.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/EditOdsInstance.cs index 2cd5874f6..13c0a71e4 100644 --- a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/EditOdsInstance.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/EditOdsInstance.cs @@ -4,9 +4,17 @@ // See the LICENSE and NOTICES files in the project root for more information. using AutoMapper; -using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Infrastructure.Providers.Interfaces; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Infrastructure.Documentation; using FluentValidation; +using Microsoft.Extensions.Options; using Swashbuckle.AspNetCore.Annotations; namespace EdFi.Ods.AdminApi.Features.OdsInstances; @@ -17,78 +25,71 @@ public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder .MapPut(endpoints, "/odsInstances/{id}", Handle) - .WithDefaultDescription() + .WithDefaultSummaryAndDescription() .WithRouteOptions(b => b.WithResponseCode(200)) - .BuildForVersions(AdminApiVersions.V1); + .BuildForVersions(AdminApiVersions.V2); } - public async Task Handle( + public static async Task Handle( Validator validator, IEditOdsInstanceCommand editOdsInstanceCommand, IMapper mapper, + ISymmetricStringEncryptionProvider encryptionProvider, + IOptions options, EditOdsInstanceRequest request, - int id - ) + int id) { - request.OdsInstanceId = id; + request.Id = id; await validator.GuardAsync(request); - var updatedOdsInstance = editOdsInstanceCommand.Execute(request); - var model = mapper.Map(updatedOdsInstance); - return AdminApiResponse.Updated(model, "odsInstance"); + + string encryptionKey = options.Value.EncryptionKey ?? throw new InvalidOperationException("EncryptionKey can't be null."); + if (!string.IsNullOrEmpty(request.ConnectionString)) + request.ConnectionString = encryptionProvider.Encrypt(request.ConnectionString, Convert.FromBase64String(encryptionKey)); + else + request.ConnectionString = string.Empty; + editOdsInstanceCommand.Execute(request); + return Results.Ok(); } [SwaggerSchema(Title = "EditOdsInstanceRequest")] public class EditOdsInstanceRequest : IEditOdsInstanceModel { - [SwaggerSchema(Description = FeatureConstants.OdsInstanceIdDescription, Nullable = false)] - public int OdsInstanceId { get; set; } - [SwaggerSchema(Description = FeatureConstants.OdsInstanceName, Nullable = false)] public string? Name { get; set; } - [SwaggerSchema(Description = FeatureConstants.OdsInstanceInstanceType, Nullable = true)] public string? InstanceType { get; set; } - - [SwaggerSchema(Description = FeatureConstants.OdsInstanceStatus, Nullable = true)] - public string? Status { get; set; } - - [SwaggerSchema(Description = FeatureConstants.OdsInstanceIsExtended, Nullable = true)] - public bool? IsExtended { get; set; } - - [SwaggerSchema(Description = FeatureConstants.OdsInstanceVersion, Nullable = true)] - public string? Version { get; set; } + [SwaggerSchema(Description = FeatureConstants.OdsInstanceConnectionString, Nullable = true)] + public string? ConnectionString { get; set; } + [SwaggerExclude] + public int Id { get; set; } } public class Validator : AbstractValidator { private readonly IGetOdsInstancesQuery _getOdsInstancesQuery; private readonly IGetOdsInstanceQuery _getOdsInstanceQuery; + private readonly string _databaseEngine; - public Validator(IGetOdsInstancesQuery getOdsInstancesQuery, IGetOdsInstanceQuery getOdsInstanceQuery) + public Validator(IGetOdsInstancesQuery getOdsInstancesQuery, IGetOdsInstanceQuery getOdsInstanceQuery, IOptions options) { _getOdsInstancesQuery = getOdsInstancesQuery; _getOdsInstanceQuery = getOdsInstanceQuery; - - RuleFor(m => m.OdsInstanceId) - .Must(id => id > 0) - .WithMessage("Please provide valid Ods instance Id."); + _databaseEngine = options.Value.DatabaseEngine ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); RuleFor(m => m.Name) .NotEmpty() .Must(BeAUniqueName) - .WithMessage(FeatureConstants.OdsInstanceAlreadyExistsMessage) - .When(m => BeAnExistingOdsInstance(m.OdsInstanceId) && NameIsChanged(m)); - - RuleFor(m => m.Name).NotEmpty().MaximumLength(100).When(m => !string.IsNullOrEmpty(m.Name)); + .WithMessage(FeatureConstants.ClaimSetAlreadyExistsMessage) + .When(m => BeAnExistingOdsInstance(m.Id) && NameIsChanged(m)); RuleFor(m => m.InstanceType) - .NotEmpty() .MaximumLength(100) .When(m => !string.IsNullOrEmpty(m.InstanceType)); - RuleFor(m => m.Status).NotEmpty().MaximumLength(100).When(m => !string.IsNullOrEmpty(m.Status)); - - RuleFor(m => m.Version).NotEmpty().MaximumLength(20).When(m => !string.IsNullOrEmpty(m.Version)); + RuleFor(m => m.ConnectionString) + .Must(BeAValidConnectionString) + .WithMessage(FeatureConstants.OdsInstanceConnectionStringInvalid) + .When(m => !string.IsNullOrEmpty(m.ConnectionString)); } private bool BeAnExistingOdsInstance(int id) @@ -99,12 +100,18 @@ private bool BeAnExistingOdsInstance(int id) private bool NameIsChanged(IEditOdsInstanceModel model) { - return _getOdsInstanceQuery.Execute(model.OdsInstanceId).Name != model.Name; + return _getOdsInstanceQuery.Execute(model.Id).Name != model.Name; } private bool BeAUniqueName(string? name) { return _getOdsInstancesQuery.Execute().TrueForAll(x => x.Name != name); } + private bool BeAValidConnectionString(string? connectionString) + { + return ConnectionStringHelper.ValidateConnectionString(_databaseEngine, connectionString); + } + } } + diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/OdsInstanceModel.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/OdsInstanceModel.cs index cd2901a2d..ce16db0f9 100644 --- a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/OdsInstanceModel.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/OdsInstanceModel.cs @@ -3,19 +3,25 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.AdminApi.Features.OdsInstanceContext; +using EdFi.Ods.AdminApi.Features.OdsInstanceDerivative; using Swashbuckle.AspNetCore.Annotations; -using System.Text.Json.Serialization; - -namespace EdFi.Ods.AdminApi.Features.OdsInstances; +using System.Text.Json.Serialization; + +namespace EdFi.Ods.AdminApi.Features.ODSInstances; [SwaggerSchema(Title = "OdsInstance")] public class OdsInstanceModel -{ +{ [JsonPropertyName("id")] public int OdsInstanceId { get; set; } - public string? Name { get; set; } + public string Name { get; set; } = string.Empty; public string? InstanceType { get; set; } - public string? Version { get; set; } - public bool? IsExtended { get; set; } - public string? Status { get; set; } +} + +[SwaggerSchema(Title = "OdsInstanceDetail")] +public class OdsInstanceDetailModel : OdsInstanceModel +{ + public IEnumerable? OdsInstanceContexts { get; set; } + public IEnumerable? OdsInstanceDerivatives { get; set; } } diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/ReadOdsInstance.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/ReadOdsInstance.cs index 8bff668da..d72cb7405 100644 --- a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/ReadOdsInstance.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/ReadOdsInstance.cs @@ -4,41 +4,41 @@ // See the LICENSE and NOTICES files in the project root for more information. using AutoMapper; -using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -namespace EdFi.Ods.AdminApi.Features.OdsInstances; +namespace EdFi.Ods.AdminApi.Features.ODSInstances; public class ReadOdsInstance : IFeature { public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstances", GetOdsInstances) - .WithDefaultDescription() + .WithDefaultSummaryAndDescription() .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V1); + .BuildForVersions(AdminApiVersions.V2); AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstances/{id}", GetOdsInstance) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V1); - } + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } - internal Task GetOdsInstances(IGetOdsInstancesQuery getOdsInstancesQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams) + internal static Task GetOdsInstances(IGetOdsInstancesQuery getOdsInstancesQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams, int? id, string? name, string? instanceType) { - var odsInstances = mapper.Map>(getOdsInstancesQuery.Execute( - commonQueryParams)); - return Task.FromResult(AdminApiResponse>.Ok(odsInstances)); + var odsInstances = mapper.Map>(getOdsInstancesQuery.Execute( + commonQueryParams, + id, + name, + instanceType)); + return Task.FromResult(Results.Ok(odsInstances)); } - internal Task GetOdsInstance(IGetOdsInstanceQuery getOdsInstanceQuery, IMapper mapper, int id) + internal static Task GetOdsInstance(IGetOdsInstanceQuery getOdsInstanceQuery, IMapper mapper, int id) { var odsInstance = getOdsInstanceQuery.Execute(id); - if (odsInstance == null) - { - throw new NotFoundException("odsInstance", id); - } - var model = mapper.Map(odsInstance); - return Task.FromResult(AdminApiResponse.Ok(model)); + var model = mapper.Map(odsInstance); + return Task.FromResult(Results.Ok(model)); } } diff --git a/Application/EdFi.Ods.AdminApi/Features/Profiles/AddProfile.cs b/Application/EdFi.Ods.AdminApi/Features/Profiles/AddProfile.cs new file mode 100644 index 000000000..c3d0cdeb9 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Profiles/AddProfile.cs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Infrastructure.Documentation; +using FluentValidation; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.Profiles; + +public class AddProfile : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapPost(endpoints, "/profiles", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(201)) + .BuildForVersions(AdminApiVersions.V2); + } + + [ProfileRequestExample] + public async Task Handle(Validator validator, IAddProfileCommand addProfileCommand, IMapper mapper, AddProfileRequest request) + { + await validator.GuardAsync(request); + var addedProfile = addProfileCommand.Execute(request); + return Results.Created($"/profiles/{addedProfile.ProfileId}", null); + } + + [SwaggerSchema(Title = "AddProfileRequest")] + public class AddProfileRequest : IAddProfileModel + { + [SwaggerSchema(Description = FeatureConstants.ProfileName, Nullable = false)] + public string? Name { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ProfileDefinition, Nullable = false)] + public string? Definition { get; set; } + } + + public class Validator : AbstractValidator + { + private readonly IGetProfilesQuery _getProfilesQuery; + + public Validator(IGetProfilesQuery getProfilesQuery) + { + _getProfilesQuery = getProfilesQuery; + + RuleFor(m => m.Name).NotEmpty(); + + RuleFor(m => m.Name) + .Must(BeAUniqueName) + .WithMessage(FeatureConstants.ProfileAlreadyExistsMessage); + + RuleFor(m => m.Definition).NotEmpty(); + + RuleFor(m => m).Custom((profile, context) => + { + if (!string.IsNullOrEmpty(profile.Definition)) + { + var validator = new ProfileValidator(); + validator.Validate(profile.Name!, profile.Definition, context); + } + }); + } + + private bool BeAUniqueName(string? profileName) + { + return _getProfilesQuery.Execute().TrueForAll(x => x.ProfileName != profileName); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Profiles/DeleteProfile.cs b/Application/EdFi.Ods.AdminApi/Features/Profiles/DeleteProfile.cs new file mode 100644 index 000000000..3becd9ec7 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Profiles/DeleteProfile.cs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +namespace EdFi.Ods.AdminApi.Features.Vendors; + +public class DeleteProfile : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapDelete(endpoints, "/profiles/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureCommonConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static Task Handle(IDeleteProfileCommand deleteProfileCommand, int id) + { + deleteProfileCommand.Execute(id); + return Task.FromResult(Results.Ok()); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Profiles/EditProfile.cs b/Application/EdFi.Ods.AdminApi/Features/Profiles/EditProfile.cs new file mode 100644 index 000000000..4a2622da8 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Profiles/EditProfile.cs @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Infrastructure.Documentation; +using FluentValidation; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.Profiles; + +public class EditProfile : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapPut(endpoints, "/profiles/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + [ProfileRequestExample] + public async Task Handle(Validator validator, IEditProfileCommand editProfileCommand, IMapper mapper, EditProfileRequest request, int id) + { + request.Id = id; + await validator.GuardAsync(request); + request.Id = id; + editProfileCommand.Execute(request); + return Results.Ok(); + } + + [SwaggerSchema(Title = "EditProfileRequest")] + public class EditProfileRequest : IEditProfileModel + { + [SwaggerSchema(Description = FeatureConstants.ProfileName, Nullable = false)] + public string? Name { get; set; } + + [SwaggerSchema(Description = FeatureConstants.ProfileDefinition, Nullable = false)] + public string? Definition { get; set; } + + [SwaggerExclude] + [SwaggerSchema(Description = FeatureConstants.ProfileIdDescription, Nullable = false)] + public int Id { get; set; } + } + + public class Validator : AbstractValidator + { + private readonly IGetProfilesQuery _getProfilesQuery; + private readonly IGetProfileByIdQuery _getProfileByIdQuery; + + public Validator(IGetProfilesQuery getProfilesQuery, IGetProfileByIdQuery getProfileByIdQuery) + { + _getProfilesQuery = getProfilesQuery; + _getProfileByIdQuery = getProfileByIdQuery; + + RuleFor(m => m.Name).NotEmpty(); + + RuleFor(m => m.Name) + .Must(BeAUniqueName) + .WithMessage(FeatureConstants.AnotherProfileAlreadyExistsMessage) + .When(m => !string.IsNullOrEmpty(m.Name) && BeAnExistingOdsInstance(m.Id) && NameIsChanged(m)); + + RuleFor(m => m.Definition).NotEmpty(); + + RuleFor(m => m).Custom((profile, context) => + { + if (!string.IsNullOrEmpty(profile.Definition)) + { + var validator = new ProfileValidator(); + validator.Validate(profile.Name!, profile.Definition, context); + } + }); + } + + private bool BeAnExistingOdsInstance(int id) + { + _getProfileByIdQuery.Execute(id); + return true; + } + + private bool NameIsChanged(IEditProfileModel model) + { + return _getProfileByIdQuery.Execute(model.Id).ProfileName != model.Name; + } + + + private bool BeAUniqueName(string? profileName) + { + return _getProfilesQuery.Execute().TrueForAll(x => x.ProfileName != profileName); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Profiles/ProfileModel.cs b/Application/EdFi.Ods.AdminApi/Features/Profiles/ProfileModel.cs new file mode 100644 index 000000000..4b813d0eb --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Profiles/ProfileModel.cs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.Profiles; + +[SwaggerSchema(Title = "Profile")] +public class ProfileModel +{ + public int? Id { get; set; } + public string? Name { get; set; } +} + +[SwaggerSchema(Title = "ProfileDetails")] +public class ProfileDetailsModel : ProfileModel +{ + public string? Definition { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Profiles/ProfileValidator.cs b/Application/EdFi.Ods.AdminApi/Features/Profiles/ProfileValidator.cs new file mode 100644 index 000000000..5d58ef0e3 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Profiles/ProfileValidator.cs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using FluentValidation; +using System.Reflection; +using System.Xml; +using System.Xml.Schema; + +namespace EdFi.Ods.AdminApi.Features.Profiles +{ + public class ProfileValidator + { + public void Validate(string name, string definition, ValidationContext context) + { + var schema = new XmlSchemaSet(); + var path = new Uri(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!).LocalPath; + schema.Add("", Path.Combine(path, "Schema", "Ed-Fi-ODS-API-Profile.xsd")); + var propertyName = "Definition"; + + void EventHandler(object? sender, ValidationEventArgs e) + { + if (e.Severity == XmlSeverityType.Error) + { + context.AddFailure(propertyName, e.Message); + } + } + try + { + var document = new XmlDocument(); + document.LoadXml(definition); + document.Schemas.Add(schema); + document.Validate(EventHandler); + + var profile = document.DocumentElement; + if (profile != null && !string.IsNullOrEmpty(name)) + { + var profileName = profile.GetAttribute("name"); + if(!profileName.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + { + context.AddFailure(propertyName, $"Profile name attribute value should match with {name}." ); + } + } + } + catch (Exception ex) + { + context.AddFailure(propertyName, ex.Message.ToString()); + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Profiles/ReadProfile.cs b/Application/EdFi.Ods.AdminApi/Features/Profiles/ReadProfile.cs new file mode 100644 index 000000000..04e2af954 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Profiles/ReadProfile.cs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; + +namespace EdFi.Ods.AdminApi.Features.Profiles; + +public class ReadProfile : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/profiles", GetProfiles) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + + AdminApiEndpointBuilder.MapGet(endpoints, "/profiles/{id}", GetProfile) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static Task GetProfiles(IGetProfilesQuery getProfilesQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams, int? id, string? name) + { + var profileList = mapper.Map>(getProfilesQuery.Execute( + commonQueryParams, + id, name)); + return Task.FromResult(Results.Ok(profileList)); + } + + internal static Task GetProfile(IGetProfileByIdQuery getProfileByIdQuery, IMapper mapper, int id) + { + var profile = getProfileByIdQuery.Execute(id); + if (profile == null) + { + throw new NotFoundException("profile", id); + } + var model = mapper.Map(profile); + return Task.FromResult(Results.Ok(model)); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/RequestLoggingMiddleware.cs b/Application/EdFi.Ods.AdminApi/Features/RequestLoggingMiddleware.cs index 63c130460..73050e0f1 100644 --- a/Application/EdFi.Ods.AdminApi/Features/RequestLoggingMiddleware.cs +++ b/Application/EdFi.Ods.AdminApi/Features/RequestLoggingMiddleware.cs @@ -1,94 +1,224 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Common.Utils.Extensions; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using FluentValidation; -using System.Net; -using System.Text.Json; - -namespace EdFi.Ods.AdminApi.Features; - -public class RequestLoggingMiddleware -{ - private readonly RequestDelegate _next; - - public RequestLoggingMiddleware(RequestDelegate next) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - } - - public async Task Invoke(HttpContext context, ILogger logger) - { - try - { - if (context.Request.Path.StartsWithSegments(new PathString("/.well-known"))) - { - // Requests to the OpenId Connect ".well-known" endpoint are too chatty for informational logging, but could be useful in debug logging. - logger.LogDebug(JsonSerializer.Serialize(new { path = context.Request.Path.Value, traceId = context.TraceIdentifier })); - } - else - { - logger.LogInformation(JsonSerializer.Serialize(new { path = context.Request.Path.Value, traceId = context.TraceIdentifier })); - } - await _next(context); - } - catch (Exception ex) - { - var response = context.Response; - response.ContentType = "application/json"; - - switch (ex) - { - case ValidationException validationException: - var validationResponse = new - { - title = "Validation failed", - status = (int)HttpStatusCode.BadRequest, - errors = new Dictionary>() - }; - - validationException.Errors.ForEach(x => - { - if (!validationResponse.errors.ContainsKey(x.PropertyName)) - { - validationResponse.errors[x.PropertyName] = new List(); - } - validationResponse.errors[x.PropertyName].Add(x.ErrorMessage.Replace("\u0027", "'")); - }); - - logger.LogDebug(JsonSerializer.Serialize(new { message = validationResponse, traceId = context.TraceIdentifier })); - - response.StatusCode = (int)HttpStatusCode.BadRequest; - await response.WriteAsync(JsonSerializer.Serialize(validationResponse)); - break; - - case INotFoundException notFoundException: - var notFoundResponse = new - { - title = notFoundException.Message, - status = (int)HttpStatusCode.NotFound - }; - logger.LogDebug(JsonSerializer.Serialize(new { message = notFoundResponse, traceId = context.TraceIdentifier })); - - response.StatusCode = (int)HttpStatusCode.NotFound; - await response.WriteAsync(JsonSerializer.Serialize(notFoundResponse)); - break; - - case IAdminApiException adminApiException: - logger.LogError(JsonSerializer.Serialize(new { message = "An uncaught error has occurred", error = new { ex.Message, ex.StackTrace }, traceId = context.TraceIdentifier })); - response.StatusCode = adminApiException.StatusCode.HasValue ? (int)adminApiException.StatusCode : 500; - await response.WriteAsync(JsonSerializer.Serialize(new { message = "The server encountered an unexpected condition that prevented it from fulfilling the request." })); - break; - - default: - response.StatusCode = (int)HttpStatusCode.InternalServerError; - logger.LogError(JsonSerializer.Serialize(new { message = "An uncaught error has occurred", error = new { ex.Message, ex.StackTrace }, traceId = context.TraceIdentifier })); - await response.WriteAsync(JsonSerializer.Serialize(new { message = "The server encountered an unexpected condition that prevented it from fulfilling the request." })); - break; - } - } - } -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; +using System.Text.Json; +using EdFi.Common.Utils.Extensions; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using FluentValidation; +using log4net; + +namespace EdFi.Ods.AdminApi.Features; + +public class RequestLoggingMiddleware +{ + private readonly RequestDelegate _next; + private static readonly ILog _logger = LogManager.GetLogger(typeof(RequestLoggingMiddleware)); + + public RequestLoggingMiddleware(RequestDelegate next) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + } + + public async Task Invoke(HttpContext context) + { + try + { + if (context.Request.Path.StartsWithSegments(new PathString("/.well-known"))) + { + // Requests to the OpenId Connect ".well-known" endpoint are too chatty for informational logging, but could be useful in debug logging. + _logger.Debug( + JsonSerializer.Serialize( + new { path = context.Request.Path.Value, traceId = context.TraceIdentifier } + ) + ); + } + else + { + _logger.Info( + JsonSerializer.Serialize( + new { path = context.Request.Path.Value, traceId = context.TraceIdentifier } + ) + ); + } + + // Check if this is a token endpoint request and intercept the response + if (context.Request.Path.StartsWithSegments("/connect/token")) + { + // Capture the original response body stream + var originalBodyStream = context.Response.Body; + + using var responseBody = new MemoryStream(); + context.Response.Body = responseBody; + + try + { + // Execute the next middleware + await _next(context); + + // Check if response is 400 and contains invalid_scope error + if (context.Response.StatusCode == 400) + { + responseBody.Seek(0, SeekOrigin.Begin); + var responseContent = await new StreamReader(responseBody).ReadToEndAsync(); + + // Check if the response contains invalid_scope error + if (responseContent.Contains("\"error\": \"invalid_scope\"")) + { + context.Response.ContentType = "application/problem+json"; + } + + // Write the response back to the original stream + responseBody.Seek(0, SeekOrigin.Begin); + await responseBody.CopyToAsync(originalBodyStream); + } + else + { + // For non-400 responses, just copy the response back + responseBody.Seek(0, SeekOrigin.Begin); + await responseBody.CopyToAsync(originalBodyStream); + } + } + finally + { + // Restore the original response body stream + context.Response.Body = originalBodyStream; + } + } + else + { + await _next(context); + } + } + catch (Exception ex) + { + var response = context.Response; + + // Check if response has already started or stream is closed + if (response.HasStarted) + { + _logger.Error( + JsonSerializer.Serialize( + new + { + message = "Cannot write to response, response has already started", + error = new { ex.Message, ex.StackTrace }, + traceId = context.TraceIdentifier + } + ), + ex + ); + return; + } + + response.ContentType = "application/problem+json"; + + switch (ex) + { + case ValidationException validationException: + var validationResponse = new + { + title = "Validation failed", + errors = new Dictionary>() + }; + + validationException.Errors.ForEach(x => + { + if (!validationResponse.errors.ContainsKey(x.PropertyName)) + { + validationResponse.errors[x.PropertyName] = new List(); + } + validationResponse.errors[x.PropertyName].Add(x.ErrorMessage.Replace("\u0027", "'")); + }); + +#pragma warning disable S6667 // Logging in a catch clause should pass the caught exception as a parameter. + _logger.Debug( + JsonSerializer.Serialize( + new { message = validationResponse, traceId = context.TraceIdentifier } + ) + ); +#pragma warning restore S6667 // Logging in a catch clause should pass the caught exception as a parameter. + + response.StatusCode = (int)HttpStatusCode.BadRequest; + await response.WriteAsync(JsonSerializer.Serialize(validationResponse)); + break; + + case INotFoundException notFoundException: + var notFoundResponse = new { title = notFoundException.Message, }; + _logger.Debug( + JsonSerializer.Serialize( + new { message = notFoundResponse, traceId = context.TraceIdentifier } + ) + ); + + response.StatusCode = (int)HttpStatusCode.NotFound; + await response.WriteAsync(JsonSerializer.Serialize(notFoundResponse)); + break; + + case IAdminApiException adminApiException: + var message = + adminApiException.StatusCode.HasValue + && !string.IsNullOrWhiteSpace(adminApiException.Message) + ? adminApiException.Message + : "The server encountered an unexpected condition that prevented it from fulfilling the request."; + _logger.Error( + JsonSerializer.Serialize( + new + { + message = "An uncaught error has occurred", + error = new { ex.Message, ex.StackTrace }, + traceId = context.TraceIdentifier + } + ), + ex + ); + response.StatusCode = adminApiException.StatusCode.HasValue + ? (int)adminApiException.StatusCode + : 500; + await response.WriteAsync(JsonSerializer.Serialize(new { message = message })); + break; + + case BadHttpRequestException: + _logger.Error( + JsonSerializer.Serialize( + new + { + message = "The request body contains malformed JSON. Please ensure your data is properly formatted and try again.", + error = new { ex.Message, ex.StackTrace }, + traceId = context.TraceIdentifier + } + ), + ex + ); + response.StatusCode = (int)HttpStatusCode.BadRequest; + await response.WriteAsync(JsonSerializer.Serialize(new { message = "The request body contains malformed JSON. Please ensure your data is properly formatted and try again." })); + break; + + default: + _logger.Error( + JsonSerializer.Serialize( + new + { + message = "An uncaught error has occurred", + error = new { ex.Message, ex.StackTrace }, + traceId = context.TraceIdentifier + } + ), + ex + ); + response.StatusCode = (int)HttpStatusCode.InternalServerError; + await response.WriteAsync( + JsonSerializer.Serialize( + new + { + message = "The server encountered an unexpected condition that prevented it from fulfilling the request." + } + ) + ); + break; + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ResourceClaimActionAuthStrategies/ReadResourceClaimActionAuthStrategies.cs b/Application/EdFi.Ods.AdminApi/Features/ResourceClaimActionAuthStrategies/ReadResourceClaimActionAuthStrategies.cs new file mode 100644 index 000000000..73e6ff48f --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ResourceClaimActionAuthStrategies/ReadResourceClaimActionAuthStrategies.cs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +namespace EdFi.Ods.AdminApi.Features.ResourceClaimActionAuthStrategies; + +public class ReadResourceClaimActionAuthStrategies : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/resourceClaimActionAuthStrategies", GetResourceClaimActionAuthorizationStrategies) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse>(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static Task GetResourceClaimActionAuthorizationStrategies(IGetResourceClaimActionAuthorizationStrategiesQuery getResourceClaimActionAuthorizationStrategiesQuery, [AsParameters] CommonQueryParams commonQueryParams, string? resourceName) + { + var resourceClaims = getResourceClaimActionAuthorizationStrategiesQuery.Execute(commonQueryParams, resourceName); + return Task.FromResult(Results.Ok(resourceClaims)); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ResourceClaimActionAuthStrategies/ResourceClaimActionAuthStrategyModel.cs b/Application/EdFi.Ods.AdminApi/Features/ResourceClaimActionAuthStrategies/ResourceClaimActionAuthStrategyModel.cs new file mode 100644 index 000000000..016b4f160 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ResourceClaimActionAuthStrategies/ResourceClaimActionAuthStrategyModel.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Features.ResourceClaimActionAuthStrategies +{ + public class ResourceClaimActionAuthStrategyModel + { + public int ResourceClaimId { get; set; } + public string ResourceName { get; set; } = string.Empty; + + public string ClaimName { get; set; } = string.Empty; + + public IReadOnlyList AuthorizationStrategiesForActions { get; set; } = new List(); + } + + public class ActionWithAuthorizationStrategy + { + public int ActionId { get; set; } + public string ActionName { get; set; } = string.Empty; + public IReadOnlyList AuthorizationStrategies { get; set; } = new List(); + + } + + public class AuthorizationStrategyModelForAction + { + public int AuthStrategyId { get; set; } + public string AuthStrategyName { get; set; } = string.Empty; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ResourceClaimActions/ReadResourceClaimActions.cs b/Application/EdFi.Ods.AdminApi/Features/ResourceClaimActions/ReadResourceClaimActions.cs new file mode 100644 index 000000000..c9d237fb3 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ResourceClaimActions/ReadResourceClaimActions.cs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +namespace EdFi.Ods.AdminApi.Features.ResourceClaimActions; + +public class ReadResourceClaimActions : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/resourceClaimActions", GetResourceClaimsActions) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse>(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static Task GetResourceClaimsActions(IGetResourceClaimActionsQuery getResourceClaimActionsQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams, string? resourceName) + { + var resourceClaimActions = getResourceClaimActionsQuery.Execute(commonQueryParams, resourceName); + return Task.FromResult(Results.Ok(resourceClaimActions)); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ResourceClaimActions/ResourceClaimActionModel.cs b/Application/EdFi.Ods.AdminApi/Features/ResourceClaimActions/ResourceClaimActionModel.cs new file mode 100644 index 000000000..c9bce497c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ResourceClaimActions/ResourceClaimActionModel.cs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Features.ResourceClaimActions +{ + public class ResourceClaimActionModel + { + public int ResourceClaimId { get; set; } + public string ResourceName { get; set; } = string.Empty; + public string ClaimName { get; set; } = string.Empty; + public List Actions { get; set; } = new List(); + } + + public class ActionForResourceClaimModel + { + public string Name { get; set; } = string.Empty; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/ResourceClaims/ReadResourceClaims.cs b/Application/EdFi.Ods.AdminApi/Features/ResourceClaims/ReadResourceClaims.cs new file mode 100644 index 000000000..cfea02ffe --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/ResourceClaims/ReadResourceClaims.cs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Features.ClaimSets; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; + +namespace EdFi.Ods.AdminApi.Features.ResourceClaims; + +public class ReadResourceClaims : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/resourceClaims", GetResourceClaims) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse>(200)) + .BuildForVersions(AdminApiVersions.V2); + + AdminApiEndpointBuilder.MapGet(endpoints, "/resourceClaims/{id}", GetResourceClaim) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + internal static Task GetResourceClaims(IGetResourceClaimsQuery getResourceClaimsQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams, int? id, string? name) + { + var resourceClaims = mapper.Map>(getResourceClaimsQuery.Execute( + commonQueryParams, + id, name)); + + return Task.FromResult(Results.Ok(resourceClaims)); + } + + internal static Task GetResourceClaim(IGetResourceClaimByResourceClaimIdQuery getResourceClaimByResourceClaimIdQuery, IMapper mapper, int id) + { + var resourceClaim = getResourceClaimByResourceClaimIdQuery.Execute(id); + if (resourceClaim == null) + { + throw new NotFoundException("resourceclaim", id); + } + var model = mapper.Map(resourceClaim); + return Task.FromResult(Results.Ok(model)); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs b/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs new file mode 100644 index 000000000..7ac784734 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Features.Tenants; + +public class ReadTenants : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapGet(endpoints, "/tenants", GetTenantsAsync) + .BuildForVersions(AdminApiVersions.V2); + + AdminApiEndpointBuilder + .MapGet(endpoints, "/tenants/{tenantName}", GetTenantsByTenantIdAsync) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task GetTenantsAsync( + [FromServices] ITenantsService tenantsService, + IMemoryCache memoryCache, + IOptions options + ) + { + var _databaseEngine = + options.Value.DatabaseEngine + ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); + + var tenants = await tenantsService.GetTenantsAsync(true); + + var response = tenants + .Select(t => + { + var adminHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase( + _databaseEngine, + t.ConnectionStrings.EdFiAdminConnectionString + ); + var securityHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase( + _databaseEngine, + t.ConnectionStrings.EdFiSecurityConnectionString + ); + + return new TenantsResponse + { + TenantName = t.TenantName, + AdminConnectionString = new EdfiConnectionString() + { + host = adminHostAndDatabase.Host, + database = adminHostAndDatabase.Database + }, + SecurityConnectionString = new EdfiConnectionString() + { + host = securityHostAndDatabase.Host, + database = securityHostAndDatabase.Database + } + }; + }) + .ToList(); + + return Results.Ok(response); + } + + public static async Task GetTenantsByTenantIdAsync( + [FromServices] ITenantsService tenantsService, + IMemoryCache memoryCache, + string tenantName, + IOptions options + ) + { + var _databaseEngine = + options.Value.DatabaseEngine + ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); + + var tenant = await tenantsService.GetTenantByTenantIdAsync(tenantName); + if (tenant == null) + return Results.NotFound(); + + var adminHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase( + _databaseEngine, + tenant.ConnectionStrings.EdFiAdminConnectionString + ); + var securityHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase( + _databaseEngine, + tenant.ConnectionStrings.EdFiSecurityConnectionString + ); + + return Results.Ok( + new TenantsResponse + { + TenantName = tenant.TenantName, + AdminConnectionString = new EdfiConnectionString() + { + host = adminHostAndDatabase.Host, + database = adminHostAndDatabase.Database + }, + SecurityConnectionString = new EdfiConnectionString() + { + host = securityHostAndDatabase.Host, + database = securityHostAndDatabase.Database + } + } + ); + } +} + +public class TenantsResponse +{ + public string? TenantName { get; set; } + public EdfiConnectionString? AdminConnectionString { get; set; } + public EdfiConnectionString? SecurityConnectionString { get; set; } +} + +public class EdfiConnectionString +{ + public string? host { get; set; } + public string? database { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantModel.cs b/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantModel.cs new file mode 100644 index 000000000..0e9ee8dd5 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantModel.cs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Constants; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.Tenants; + +[SwaggerSchema] +public class TenantModel +{ + [SwaggerSchema(Description = Constants.TenantNameDescription, Nullable = false)] + public required string TenantName { get; set; } + + [SwaggerSchema(Description = Constants.TenantConnectionStringDescription, Nullable = false)] + public TenantModelConnectionStrings ConnectionStrings { get; set; } = new(); +} + +[SwaggerSchema] +public class TenantModelConnectionStrings +{ + public string EdFiSecurityConnectionString { get; set; } + public string EdFiAdminConnectionString { get; set; } + + public TenantModelConnectionStrings() + { + EdFiAdminConnectionString = string.Empty; + EdFiSecurityConnectionString = string.Empty; + } + + public TenantModelConnectionStrings(string edFiAdminConnectionString, string edFiSecurityConnectionString) + { + EdFiAdminConnectionString = edFiAdminConnectionString; + EdFiSecurityConnectionString = edFiSecurityConnectionString; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Vendors/AddVendor.cs b/Application/EdFi.Ods.AdminApi/Features/Vendors/AddVendor.cs index 1bc0fccb1..9c22c809e 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Vendors/AddVendor.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Vendors/AddVendor.cs @@ -4,7 +4,9 @@ // See the LICENSE and NOTICES files in the project root for more information. using AutoMapper; -using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; using FluentValidation; @@ -18,21 +20,20 @@ public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder .MapPost(endpoints, "/vendors", Handle) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponse(201)) - .BuildForVersions(AdminApiVersions.V1); + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(201)) + .BuildForVersions(AdminApiVersions.V2); } - public async Task Handle(Validator validator, AddVendorCommand addVendorCommand, IMapper mapper, Request request) + public async static Task Handle(Validator validator, AddVendorCommand addVendorCommand, IMapper mapper, AddVendorRequest request) { await validator.GuardAsync(request); var addedVendor = addVendorCommand.Execute(request); - var model = mapper.Map(addedVendor); - return AdminApiResponse.Created(model, "Vendor", $"/vendors/{model.VendorId}"); + return Results.Created($"/vendors/{addedVendor.VendorId}", null); } [SwaggerSchema(Title = "AddVendorRequest")] - public class Request : IAddVendorModel + public class AddVendorRequest : IAddVendorModel { [SwaggerSchema(Description = FeatureConstants.VendorNameDescription, Nullable = false)] public string? Company { get; set; } @@ -47,7 +48,7 @@ public class Request : IAddVendorModel public string? ContactEmailAddress { get; set; } } - public class Validator : AbstractValidator + public class Validator : AbstractValidator { public Validator() { @@ -58,6 +59,22 @@ public Validator() RuleFor(m => m.ContactName).NotEmpty(); RuleFor(m => m.ContactEmailAddress).NotEmpty().EmailAddress(); + + RuleFor(m => m.NamespacePrefixes).Must((vendorNamespacePrefixes) => HaveACorrectLength(vendorNamespacePrefixes)) + .WithMessage(p => $"'{p.NamespacePrefixes}' exceeds maximum length"); + } + + private bool HaveACorrectLength(string? vendorNamespacePrefixes) + { + var namespacePrefixes = vendorNamespacePrefixes?.Split(",") + .Where(namespacePrefix => !string.IsNullOrWhiteSpace(namespacePrefix)) + .Select(namespacePrefix => new VendorNamespacePrefix + { + NamespacePrefix = namespacePrefix.Trim() + }) + .ToList(); + + return namespacePrefixes == null || !namespacePrefixes.Exists(m => m.NamespacePrefix.Length > 255); } } } diff --git a/Application/EdFi.Ods.AdminApi/Features/Vendors/DeleteVendor.cs b/Application/EdFi.Ods.AdminApi/Features/Vendors/DeleteVendor.cs index c427319a0..6417f6c62 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Vendors/DeleteVendor.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Vendors/DeleteVendor.cs @@ -3,9 +3,13 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; - +using EdFi.Ods.AdminApi.Infrastructure.Extensions; + namespace EdFi.Ods.AdminApi.Features.Vendors; public class DeleteVendor : IFeature @@ -13,14 +17,14 @@ public class DeleteVendor : IFeature public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder.MapDelete(endpoints, "/vendors/{id}", Handle) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponseCode(200, FeatureConstants.DeletedSuccessResponseDescription)) - .BuildForVersions(AdminApiVersions.V1); + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureCommonConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V2); } - public Task Handle(DeleteVendorCommand deleteVendorCommand, int id) + public static Task Handle(DeleteVendorCommand deleteVendorCommand, int id) { deleteVendorCommand.Execute(id); - return Task.FromResult(AdminApiResponse.Deleted("Vendor")); + return Task.FromResult(Results.Ok("Vendor".ToJsonObjectResponseDeleted())); } } diff --git a/Application/EdFi.Ods.AdminApi/Features/Vendors/EditVendor.cs b/Application/EdFi.Ods.AdminApi/Features/Vendors/EditVendor.cs index 3bc1b3e76..9088a353b 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Vendors/EditVendor.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Vendors/EditVendor.cs @@ -4,9 +4,12 @@ // See the LICENSE and NOTICES files in the project root for more information. using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Infrastructure.Documentation; using FluentValidation; using Swashbuckle.AspNetCore.Annotations; @@ -17,26 +20,25 @@ public class EditVendor : IFeature public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder.MapPut(endpoints, "/vendors/{id}", Handle) - .WithDefaultDescription() - .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V1); + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200)) + .BuildForVersions(AdminApiVersions.V2); } - public async Task Handle(EditVendorCommand editVendorCommand, IMapper mapper, - Validator validator, Request request, int id) + public static async Task Handle(EditVendorCommand editVendorCommand, IMapper mapper, + Validator validator, EditVendorRequest request, int id) { - request.VendorId = id; + request.Id = id; await validator.GuardAsync(request); - var updatedVendor = editVendorCommand.Execute(request); - var model = mapper.Map(updatedVendor); - return AdminApiResponse.Updated(model, "Vendor"); + editVendorCommand.Execute(request); + return Results.Ok(); } [SwaggerSchema(Title = "EditVendorRequest")] - public class Request : IEditVendor + public class EditVendorRequest : IEditVendor { - [SwaggerSchema(Description = FeatureConstants.VendorIdDescription, Nullable = false)] - public int VendorId { get; set; } + [SwaggerExclude] + public int Id { get; set; } [SwaggerSchema(Description = FeatureConstants.VendorNameDescription, Nullable = false)] public string? Company { get; set; } @@ -51,11 +53,11 @@ public class Request : IEditVendor public string? ContactEmailAddress { get; set; } } - public class Validator : AbstractValidator + public class Validator : AbstractValidator { public Validator() { - RuleFor(m => m.VendorId).Must(id => id > 0).WithMessage("Please provide valid Vendor Id."); + RuleFor(m => m.Id).Must(id => id > 0).WithMessage("Please provide valid Vendor Id."); RuleFor(m => m.Company).NotEmpty(); RuleFor(m => m.Company) .Must(name => !VendorExtensions.IsSystemReservedVendorName(name)) diff --git a/Application/EdFi.Ods.AdminApi/Features/Vendors/ReadVendor.cs b/Application/EdFi.Ods.AdminApi/Features/Vendors/ReadVendor.cs index d4f1f2636..cc033ae61 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Vendors/ReadVendor.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Vendors/ReadVendor.cs @@ -4,9 +4,13 @@ // See the LICENSE and NOTICES files in the project root for more information. using AutoMapper; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; namespace EdFi.Ods.AdminApi.Features.Vendors; @@ -15,23 +19,26 @@ public class ReadVendor : IFeature public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder.MapGet(endpoints, "/vendors", GetVendors) - .WithDefaultDescription() + .WithDefaultSummaryAndDescription() .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V1); + .BuildForVersions(AdminApiVersions.V2); AdminApiEndpointBuilder.MapGet(endpoints, "/vendors/{id}", GetVendor) - .WithDefaultDescription() + .WithDefaultSummaryAndDescription() .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V1); + .BuildForVersions(AdminApiVersions.V2); } - internal Task GetVendors(IGetVendorsQuery getVendorsQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams) + internal static Task GetVendors( + IGetVendorsQuery getVendorsQuery, IMapper mapper, [AsParameters] CommonQueryParams commonQueryParams, int? id, string? company, string? namespacePrefixes, string? contactName, string? contactEmailAddress) { - var vendorList = mapper.Map>(getVendorsQuery.Execute(commonQueryParams)); - return Task.FromResult(AdminApiResponse>.Ok(vendorList)); + var vendorList = mapper.Map>(getVendorsQuery.Execute( + commonQueryParams, + id, company, namespacePrefixes, contactName, contactEmailAddress)); + return Task.FromResult(Results.Ok(vendorList)); } - internal Task GetVendor(IGetVendorByIdQuery getVendorByIdQuery, IMapper mapper, int id) + internal static Task GetVendor(IGetVendorByIdQuery getVendorByIdQuery, IMapper mapper, int id) { var vendor = getVendorByIdQuery.Execute(id); if (vendor == null) @@ -39,6 +46,6 @@ internal Task GetVendor(IGetVendorByIdQuery getVendorByIdQuery, IMapper throw new NotFoundException("vendor", id); } var model = mapper.Map(vendor); - return Task.FromResult(AdminApiResponse.Ok(model)); + return Task.FromResult(Results.Ok(model)); } } diff --git a/Application/EdFi.Ods.AdminApi/Features/Vendors/VendorModel.cs b/Application/EdFi.Ods.AdminApi/Features/Vendors/VendorModel.cs index e9c6b4cdd..83301d8a4 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Vendors/VendorModel.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Vendors/VendorModel.cs @@ -10,7 +10,7 @@ namespace EdFi.Ods.AdminApi.Features.Vendors; [SwaggerSchema(Title = "Vendor")] public class VendorModel { - public int? VendorId { get; set; } + public int? Id { get; set; } public string? Company { get; set; } public string? NamespacePrefixes { get; set; } public string? ContactName { get; set; } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiDbContext.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiDbContext.cs index 94a616c61..45bdb70e0 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiDbContext.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiDbContext.cs @@ -3,8 +3,9 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.AdminApi.Common.Infrastructure.Database; +using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; using EdFi.Ods.AdminApi.Infrastructure.Security; -using EdFi.Ods.AdminApi.Infrastructure.Database; using Microsoft.EntityFrameworkCore; namespace EdFi.Ods.AdminApi.Infrastructure; @@ -29,7 +30,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().ToTable("Authorizations").HasKey(a => a.Id); modelBuilder.Entity().ToTable("Tokens").HasKey(t => t.Id); - var engine = _configuration.GetValue("AppSettings:DatabaseEngine"); + var engine = _configuration.Get("AppSettings:DatabaseEngine", "SqlServer"); modelBuilder.ApplyDatabaseServerSpecificConventions(engine); } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiEndpointBuilder.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiEndpointBuilder.cs deleted file mode 100644 index e9067cf06..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiEndpointBuilder.cs +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Ods.AdminApi.Features; -using EdFi.Ods.AdminApi.Infrastructure.Extensions; -using Swashbuckle.AspNetCore.Annotations; - -namespace EdFi.Ods.AdminApi.Infrastructure; - -public class AdminApiEndpointBuilder -{ - private AdminApiEndpointBuilder(IEndpointRouteBuilder endpoints, - HttpVerb verb, string route, Delegate handler) - { - _endpoints = endpoints; - _verb = verb; - _route = route.Trim('/'); - _handler = handler; - _pluralResourceName = _route.Split('/').First(); - } - - private readonly IEndpointRouteBuilder _endpoints; - private readonly HttpVerb? _verb; - private readonly string _route; - private readonly Delegate? _handler; - private readonly List> _routeOptions = new(); - private readonly string _pluralResourceName; - private bool _allowAnonymous = false; - - public static AdminApiEndpointBuilder MapGet(IEndpointRouteBuilder endpoints, string route, Delegate handler) - => new(endpoints, HttpVerb.GET, route, handler); - - public static AdminApiEndpointBuilder MapPost(IEndpointRouteBuilder endpoints, string route, Delegate handler) - => new(endpoints, HttpVerb.POST, route, handler); - - public static AdminApiEndpointBuilder MapPut(IEndpointRouteBuilder endpoints, string route, Delegate handler) - => new(endpoints, HttpVerb.PUT, route, handler); - - public static AdminApiEndpointBuilder MapDelete(IEndpointRouteBuilder endpoints, string route, Delegate handler) - => new(endpoints, HttpVerb.DELETE, route, handler); - - public void BuildForVersions(params AdminApiVersions.AdminApiVersion[] versions) - { - if (versions.Length == 0) throw new ArgumentException("Must register for at least 1 version"); - if (_route == null) throw new Exception("Invalid endpoint registration. Route must be specified"); - if (_handler == null) throw new Exception("Invalid endpoint registration. Handler must be specified"); - - foreach (var version in versions) - { - if (version == null) throw new ArgumentException("Version cannot be null"); - - var versionedRoute = $"/{version}/{_route}"; - - var builder = _verb switch - { - HttpVerb.GET => _endpoints.MapGet(versionedRoute, _handler), - HttpVerb.POST => _endpoints.MapPost(versionedRoute, _handler), - HttpVerb.PUT => _endpoints.MapPut(versionedRoute, _handler), - HttpVerb.DELETE => _endpoints.MapDelete(versionedRoute, _handler), - _ => throw new ArgumentOutOfRangeException($"Unconfigured HTTP verb for mapping: {_verb}") - }; - - if (_allowAnonymous) - { - builder.AllowAnonymous(); - } - else - { - builder.RequireAuthorization(); - } - - builder.WithGroupName(version.ToString()); - builder.WithResponseCode(401, "Unauthorized. The request requires authentication"); - builder.WithResponseCode(403, "Forbidden. The request is authenticated, but not authorized to access this resource"); - builder.WithResponseCode(500, FeatureConstants.InternalServerErrorResponseDescription); - - if (_route.Contains("id", StringComparison.InvariantCultureIgnoreCase)) - { - builder.WithResponseCode(404, "Not found. A resource with given identifier could not be found."); - } - - if (_verb is HttpVerb.PUT or HttpVerb.POST) - { - builder.WithResponseCode(400, FeatureConstants.BadRequestResponseDescription); - } - - foreach (var action in _routeOptions) - { - action(builder); - } - } - } - - public AdminApiEndpointBuilder WithRouteOptions(Action routeHandlerBuilderAction) - { - _routeOptions.Add(routeHandlerBuilderAction); - return this; - } - - public AdminApiEndpointBuilder WithDefaultDescription() - { - var description = _verb switch - { - HttpVerb.GET => _route.Contains("id") ? $"Retrieves a specific {_pluralResourceName.ToSingleEntity()} based on the identifier." : $"Retrieves all {_pluralResourceName}.", - HttpVerb.POST => $"Creates {_pluralResourceName.ToSingleEntity()} based on the supplied values.", - HttpVerb.PUT => $"Updates {_pluralResourceName.ToSingleEntity()} based on the resource identifier.", - HttpVerb.DELETE => $"Deletes an existing {_pluralResourceName.ToSingleEntity()} using the resource identifier.", - _ => throw new ArgumentOutOfRangeException($"Unconfigured HTTP verb for default description {_verb}") - }; - - return WithDescription(description); - } - - public AdminApiEndpointBuilder WithDescription(string description) - { - _routeOptions.Add(rhb => rhb.WithMetadata(new SwaggerOperationAttribute(description))); - return this; - } - - public AdminApiEndpointBuilder AllowAnonymous() - { - _allowAnonymous = true; - return this; - } - - private enum HttpVerb { GET, POST, PUT, DELETE } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/AdminConsolePostgresUsersContext.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/AdminConsolePostgresUsersContext.cs new file mode 100644 index 000000000..cc5ba0f73 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/AdminConsolePostgresUsersContext.cs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EdFi.Ods.AdminApi.Infrastructure; + +public class AdminConsolePostgresUsersContext(DbContextOptions options) : PostgresUsersContext(options) +{ + public void UseTransaction(IDbContextTransaction transaction) + { + Database.UseTransaction(transaction.GetDbTransaction()); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/AdminConsoleSqlServerUsersContext.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/AdminConsoleSqlServerUsersContext.cs new file mode 100644 index 000000000..3e063e635 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/AdminConsoleSqlServerUsersContext.cs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EdFi.Ods.AdminApi.Infrastructure; +public class AdminConsoleSqlServerUsersContext(DbContextOptions options) : SqlServerUsersContext(options) +{ + public void UseTransaction(IDbContextTransaction transaction) + { + Database.UseTransaction(transaction.GetDbTransaction()); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/AutoMapper/AdminApiMappingProfile.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/AutoMapper/AdminApiMappingProfile.cs index ff17ac753..28b303ad7 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/AutoMapper/AdminApiMappingProfile.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/AutoMapper/AdminApiMappingProfile.cs @@ -2,64 +2,74 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; using EdFi.Admin.DataAccess.Models; -using Profile = AutoMapper.Profile; -using EdFi.Ods.AdminApi.Features.Vendors; +using EdFi.Ods.AdminApi.Features.Actions; +using EdFi.Ods.AdminApi.Features.ApiClients; using EdFi.Ods.AdminApi.Features.Applications; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Features.AuthorizationStrategies; using EdFi.Ods.AdminApi.Features.ClaimSets; +using EdFi.Ods.AdminApi.Features.OdsInstanceContext; +using EdFi.Ods.AdminApi.Features.OdsInstanceDerivative; +using EdFi.Ods.AdminApi.Features.ODSInstances; +using EdFi.Ods.AdminApi.Features.Profiles; +using EdFi.Ods.AdminApi.Features.ResourceClaimActions; +using EdFi.Ods.AdminApi.Features.Vendors; +using EdFi.Ods.AdminApi.Infrastructure.AutoMapper; +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; using EdFi.Ods.AdminApi.Infrastructure.Helpers; -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; -using EdFi.Ods.AdminApi.Features.OdsInstances; -using EdFi.Common.Extensions; - +using OverrideAuthStategyOnClaimSetRequest = EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditAuthStrategy.OverrideAuthStategyOnClaimSetRequest; +using Profile = AutoMapper.Profile; + namespace EdFi.Ods.AdminApi.Infrastructure; public class AdminApiMappingProfile : Profile { public AdminApiMappingProfile() { - - CreateMap() + CreateMap() .ForMember(dst => dst.Company, opt => opt.MapFrom(src => src.VendorName)) .ForMember(dst => dst.ContactName, opt => opt.MapFrom(src => src.ContactName())) .ForMember(dst => dst.ContactEmailAddress, opt => opt.MapFrom(src => src.ContactEmail())) .ForMember(dst => dst.NamespacePrefixes, opt => opt.MapFrom(src => src.VendorNamespacePrefixes.ToCommaSeparated())); CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.VendorId)) .ForMember(dst => dst.Company, opt => opt.MapFrom(src => src.VendorName)) .ForMember(dst => dst.ContactName, opt => opt.MapFrom(src => src.ContactName())) .ForMember(dst => dst.ContactEmailAddress, opt => opt.MapFrom(src => src.ContactEmail())) .ForMember(dst => dst.NamespacePrefixes, opt => opt.MapFrom(src => src.VendorNamespacePrefixes.ToCommaSeparated())); - CreateMap() + CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.ApplicationId)) .ForMember(dst => dst.EducationOrganizationIds, opt => opt.MapFrom(src => src.EducationOrganizationIds())) - .ForMember(dst => dst.ProfileName, opt => opt.MapFrom(src => src.ProfileName())) - .ForMember(dst => dst.OdsInstanceId, opt => opt.MapFrom(src => src.OdsInstance.OdsInstanceId)) - .ForMember(dst => dst.OdsInstanceName, opt => opt.MapFrom(src => src.OdsInstanceName())) .ForMember(dst => dst.VendorId, opt => opt.MapFrom(src => src.VendorId())) - .ForMember(dst => dst.Profiles, opt => opt.MapFrom(src => src.Profiles())); - + .ForMember(dst => dst.ProfileIds, opt => opt.MapFrom(src => src.Profiles())) + .ForMember(dst => dst.Enabled, opt => opt.MapFrom(src => src.ApiClients.All(a => a.IsApproved))) + .ForMember(dst => dst.OdsInstanceIds, opt => + { + opt.ConvertUsing("ApplicationId"); + }); + + CreateMap() + .ForMember(dst => dst.ApplicationName, opt => opt.MapFrom(src => src.Name)); CreateMap() - .ForMember(dst => dst.ApplicationId, opt => opt.MapFrom(src => src.ApplicationId)) + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.ApplicationId)) + .ForMember(dst => dst.Key, opt => opt.MapFrom(src => src.Key)) + .ForMember(dst => dst.Secret, opt => opt.MapFrom(src => src.Secret)); + + CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Application.ApplicationId)) .ForMember(dst => dst.Key, opt => opt.MapFrom(src => src.Key)) .ForMember(dst => dst.Secret, opt => opt.MapFrom(src => src.Secret)); - CreateMap() + CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id)) .ForMember(dst => dst.ApplicationId, opt => opt.MapFrom(src => src.Application.ApplicationId)) .ForMember(dst => dst.Key, opt => opt.MapFrom(src => src.Key)) - .ForMember(dst => dst.Secret, opt => opt.MapFrom(src => src.Secret)); - - CreateMap() - //.ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) - .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) - .ForMember(dst => dst.InstanceType, opt => opt.MapFrom(src => src.InstanceType)) - .ForMember(dst => dst.Version, opt => opt.MapFrom(src => src.Version)) - .ForMember(dst => dst.IsExtended, opt => opt.MapFrom(src => src.IsExtended)) - .ForMember(dst => dst.Status, opt => opt.MapFrom(src => src.Status)); + .ForMember(dst => dst.Secret, opt => opt.MapFrom(src => src.Secret)); CreateMap() .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id)) @@ -68,75 +78,118 @@ public AdminApiMappingProfile() CreateMap() .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id)) - .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) .ForMember(dst => dst.IsSystemReserved, opt => opt.MapFrom(src => !src.IsEditable)); - CreateMap() + CreateMap() + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.Actions, opt => opt.MapFrom(src => src.Actions)) + .ForMember(dst => dst.AuthorizationStrategyOverridesForCRUD, opt => opt.MapFrom(src => src.AuthorizationStrategyOverridesForCRUD)) + .ForMember(dst => dst.DefaultAuthorizationStrategiesForCRUD, opt => opt.MapFrom(src => src.DefaultAuthorizationStrategiesForCRUD)) + .ForMember(dst => dst.Children, opt => opt.MapFrom(src => src.Children)); + + CreateMap() .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) - .ForMember(dst => dst.Read, opt => opt.MapFrom(src => src.Read)) - .ForMember(dst => dst.Update, opt => opt.MapFrom(src => src.Update)) - .ForMember(dst => dst.Create, opt => opt.MapFrom(src => src.Create)) - .ForMember(dst => dst.Delete, opt => opt.MapFrom(src => src.Delete)) - .ForMember(dst => dst.ReadChanges, opt => opt.MapFrom(src => src.ReadChanges)) - .ForMember(dst => dst.AuthStrategyOverridesForCRUD, opt => opt.MapFrom(src => src.AuthStrategyOverridesForCRUD)) - .ForMember(dst => dst.DefaultAuthStrategiesForCRUD, opt => opt.MapFrom(src => src.DefaultAuthStrategiesForCRUD)) - .ForMember(dst => dst.Children, opt => opt.MapFrom(src => src.Children)); - - CreateMap() - .ForMember(dst => dst.AuthStrategyId, opt => opt.MapFrom(src => src.AuthStrategyId)) - .ForMember(dst => dst.AuthStrategyName, opt => opt.MapFrom(src => src.AuthStrategyName)) - .ForMember(dst => dst.DisplayName, opt => opt.MapFrom(src => src.DisplayName)) - .ForMember(dst => dst.IsInheritedFromParent, opt => opt.MapFrom(src => src.IsInheritedFromParent)); - - CreateMap() - .ForMember(dst => dst.AuthStrategyId, opt => opt.MapFrom(src => src.AuthStrategyId)) - .ForMember(dst => dst.AuthStrategyName, opt => opt.MapFrom(src => src.AuthStrategyName)) - .ForMember(dst => dst.DisplayName, opt => opt.MapFrom(src => src.DisplayName)) - .ForMember(dst => dst.IsInheritedFromParent, opt => opt.MapFrom(src => src.IsInheritedFromParent)); - - CreateMap() + .ForMember(dst => dst.Actions, opt => opt.MapFrom(src => src.Actions)) + .ForMember(dst => dst.AuthorizationStrategyOverridesForCRUD, opt => opt.MapFrom(src => src.AuthorizationStrategyOverridesForCRUD)) + .ForMember(dst => dst.DefaultAuthorizationStrategiesForCRUD, opt => opt.MapFrom(src => src.DefaultAuthorizationStrategiesForCRUD)) + .ForMember(dst => dst.Children, opt => opt.MapFrom(src => src.Children)); + + CreateMap() + .ForMember(dst => dst.ClaimSetId, opt => opt.MapFrom(src => src.ClaimSetId)) + .ForMember(dst => dst.ResourceClaim, opt => opt.MapFrom()); + + CreateMap() + .ForMember(dst => dst.ClaimSetId, opt => opt.MapFrom(src => src.ClaimSetId)) + .ForMember(dst => dst.ResourceClaimId, opt => opt.MapFrom(src => src.ResourceClaimId)) + .ForMember(dst => dst.ActionName, opt => opt.MapFrom(src => src.ActionName)) + .ForMember(dst => dst.AuthStrategyIds, opt => { opt.ConvertUsing>("AuthorizationStrategies"); }); + + CreateMap() .ForMember(dst => dst.AuthStrategyName, opt => opt.MapFrom(src => src.AuthorizationStrategyName)) .ForMember(dst => dst.AuthStrategyId, opt => opt.MapFrom(src => src.AuthorizationStrategyId)) .ForMember(dst => dst.IsInheritedFromParent, opt => opt.Ignore()); - CreateMap() + CreateMap() .ForMember(dst => dst.AuthStrategyName, opt => opt.MapFrom(src => src.AuthorizationStrategyName)) .ForMember(dst => dst.AuthStrategyId, opt => opt.MapFrom(src => src.AuthorizationStrategyId)) - .ForMember(dst => dst.IsInheritedFromParent, opt => opt.Ignore()); - CreateMap() + .ForMember(dst => dst.DisplayName, opt => opt.MapFrom(src => src.DisplayName)); + + CreateMap() .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) - .ForMember(dst => dst.Read, opt => opt.MapFrom(src => src.Read)) - .ForMember(dst => dst.Update, opt => opt.MapFrom(src => src.Update)) - .ForMember(dst => dst.Create, opt => opt.MapFrom(src => src.Create)) - .ForMember(dst => dst.Delete, opt => opt.MapFrom(src => src.Delete)) - .ForMember(dst => dst.ReadChanges, opt => opt.MapFrom(src => src.ReadChanges)) - .ForMember(dst => dst.AuthStrategyOverridesForCRUD, opt => opt.MapFrom(src => src.AuthStrategyOverridesForCRUD)) - .ForMember(dst => dst.DefaultAuthStrategiesForCRUD, opt => opt.MapFrom(src => src.DefaultAuthStrategiesForCRUD)) - .ForMember(dst => dst.Children, opt => opt.MapFrom(src => src.Children)); - - CreateMap() - .ForMember(dst => dst.AuthorizationStrategies, opt => opt.MapFrom(src => src.AuthorizationStrategies)).ReverseMap(); - ; - - CreateMap() - .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) - .ForMember(dst => dst.Read, opt => opt.MapFrom(src => src.Read)) - .ForMember(dst => dst.Update, opt => opt.MapFrom(src => src.Update)) - .ForMember(dst => dst.Create, opt => opt.MapFrom(src => src.Create)) - .ForMember(dst => dst.Delete, opt => opt.MapFrom(src => src.Delete)) - .ForMember(dst => dst.ReadChanges, opt => opt.MapFrom(src => src.ReadChanges)) - .ForMember(dst => dst.AuthStrategyOverridesForCRUD, opt => opt.MapFrom(src => src.AuthStrategyOverridesForCRUD)) - .ForMember(dst => dst.Children, opt => opt.MapFrom(src => src.Children)); - - CreateMap() + .ForMember(dst => dst.Actions, opt => opt.MapFrom(src => src.Actions)) + .ForMember(dst => dst.AuthorizationStrategyOverridesForCRUD, opt => opt.MapFrom(src => src.AuthorizationStrategyOverridesForCRUD)) + .ForMember(dst => dst.DefaultAuthorizationStrategiesForCRUD, opt => opt.MapFrom(src => src.DefaultAuthorizationStrategiesForCRUD)) + .ForMember(dst => dst.Children, opt => opt.MapFrom(src => src.Children)); + + CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id)) .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) - .ForMember(dst => dst.Read, opt => opt.MapFrom(src => src.Read)) - .ForMember(dst => dst.Update, opt => opt.MapFrom(src => src.Update)) - .ForMember(dst => dst.Create, opt => opt.MapFrom(src => src.Create)) - .ForMember(dst => dst.Delete, opt => opt.MapFrom(src => src.Delete)) - .ForMember(dst => dst.ReadChanges, opt => opt.MapFrom(src => src.ReadChanges)) - .ForMember(dst => dst.AuthStrategyOverridesForCRUD, opt => opt.MapFrom(src => src.AuthStrategyOverridesForCRUD)) + .ForMember(dst => dst.ParentId, opt => opt.MapFrom(src => src.ParentId)) + .ForMember(dst => dst.ParentName, opt => opt.MapFrom(src => src.ParentName)) .ForMember(dst => dst.Children, opt => opt.MapFrom(src => src.Children)); + + CreateMap() + .ForMember(dst => dst.OdsInstanceId, opt => opt.MapFrom(src => src.OdsInstanceId)) + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)); + + CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.ActionId)) + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.ActionName)) + .ForMember(dst => dst.Uri, opt => opt.MapFrom(src => src.ActionUri)); + + CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.ProfileId)) + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.ProfileName)); + + CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.ProfileId)) + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.ProfileName)) + .ForMember(dst => dst.Definition, opt => opt.MapFrom(src => src.ProfileDefinition)); + + CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.OdsInstanceDerivativeId)) + .ForMember(dst => dst.OdsInstanceId, opt => opt.MapFrom(src => src.OdsInstance.OdsInstanceId)) + .ForMember(dst => dst.DerivativeType, opt => opt.MapFrom(src => src.DerivativeType)); + + CreateMap() + .ForMember(dst => dst.OdsInstanceContextId, opt => opt.MapFrom(src => src.OdsInstanceContextId)) + .ForMember(dst => dst.OdsInstanceId, opt => opt.MapFrom(src => src.OdsInstance.OdsInstanceId)) + .ForMember(dst => dst.ContextKey, opt => opt.MapFrom(src => src.ContextKey)) + .ForMember(dst => dst.ContextValue, opt => opt.MapFrom(src => src.ContextValue)); + + CreateMap() + .ForMember(dst => dst.OdsInstanceId, opt => opt.MapFrom(src => src.OdsInstanceId)) + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.OdsInstanceDerivatives, opt => opt.MapFrom(src => src.OdsInstanceDerivatives)) + .ForMember(dst => dst.OdsInstanceContexts, opt => opt.MapFrom(src => src.OdsInstanceContexts)); + + CreateMap() + .ForMember(dest => dest.ResourceClaimId, opt => opt.MapFrom(src => src.ResourceClaim.ResourceClaimId)) + .ForMember(dest => dest.ResourceName, opt => opt.MapFrom(src => src.ResourceClaim.ResourceName)) + .ForMember(dest => dest.Actions, opt => opt.Ignore());//Action is ignore as we build it manually + + CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.Key, opt => opt.MapFrom(src => src.Key)) + .ForMember(dst => dst.ApplicationId, opt => opt.MapFrom(src => src.ApplicationId)) + .ForMember(dst => dst.Secret, opt => opt.MapFrom(src => src.Secret)); + + CreateMap() + .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.ApiClientId)) + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dst => dst.Key, opt => opt.MapFrom(src => src.Key)) + .ForMember(dst => dst.ApplicationId, opt => opt.MapFrom(src => src.Application.ApplicationId)) + .ForMember(dst => dst.KeyStatus, opt => opt.MapFrom(src => src.KeyStatus)) + .ForMember(dst => dst.IsApproved, opt => opt.MapFrom(src => src.IsApproved)) + .ForMember(dst => dst.UseSandbox, opt => opt.MapFrom(src => src.UseSandbox)) + .ForMember(dst => dst.SandboxType, opt => opt.MapFrom(src => src.SandboxType)) + .ForMember(dst => dst.EducationOrganizationIds, opt => opt.MapFrom(src => src.ApplicationEducationOrganizations.Select(eu => eu.EducationOrganizationId))) + .ForMember(dst => dst.OdsInstanceIds, opt => + { + opt.ConvertUsing(src => src.ApiClientId); + }); } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/AutoMapper/Converters.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/AutoMapper/Converters.cs new file mode 100644 index 000000000..4f448fbfe --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/AutoMapper/Converters.cs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.AutoMapper; + +public class AuthStrategyIdsConverter : IValueConverter, List> +{ + private readonly IGetAllAuthorizationStrategiesQuery _getAllAuthorizationStrategiesQuery; + public AuthStrategyIdsConverter(IGetAllAuthorizationStrategiesQuery getAllAuthorizationStrategiesQuery) + { + _getAllAuthorizationStrategiesQuery = getAllAuthorizationStrategiesQuery; + } + public List Convert(List authStrategyNames, ResolutionContext context) + { + var ids = new List(); + if (authStrategyNames != null) + { + var unavailableAuthStrategies = string.Empty; + foreach (var authStrategyName in authStrategyNames) + { + var authStrategy = _getAllAuthorizationStrategiesQuery.Execute() + .FirstOrDefault(a => authStrategyName.Equals(a.AuthStrategyName, StringComparison.InvariantCultureIgnoreCase)); + + if (authStrategy == null) + { + unavailableAuthStrategies = string.Join(",", authStrategyName); + } + else + { + ids.Add(authStrategy!.AuthStrategyId); + } + } + if (!string.IsNullOrEmpty(unavailableAuthStrategies)) + { + throw new AdminApiException($"Error transforming the ID for the AuthStrategyNames {unavailableAuthStrategies!}"); + } + } + return ids; + } +} + + +public class OdsInstanceIdsForApplicationConverter : IValueConverter> +{ + private readonly IUsersContext _context; + public OdsInstanceIdsForApplicationConverter(IUsersContext context) + { + _context = context; + } + public List Convert(int applicationId, ResolutionContext context) + { + var ids = _context.ApiClientOdsInstances.Where(p => p.ApiClient.Application.ApplicationId == applicationId).Select(p => p.OdsInstance.OdsInstanceId).Distinct().ToList(); + + return ids; + } +} + +public class OdsInstanceIdsForApiClientConverter : IValueConverter> +{ + private readonly IUsersContext _context; + public OdsInstanceIdsForApiClientConverter(IUsersContext context) + { + _context = context; + } + public List Convert(int apiClientId, ResolutionContext context) + { + return _context.ApiClientOdsInstances.Where(p => p.ApiClient.ApiClientId == apiClientId).Select(p => p.OdsInstance.OdsInstanceId).ToList(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/AutoMapper/ResourceClaimResolver.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/AutoMapper/ResourceClaimResolver.cs new file mode 100644 index 000000000..45f13c03c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/AutoMapper/ResourceClaimResolver.cs @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.Features.ClaimSets; +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; + +namespace EdFi.Ods.AdminApi.Infrastructure.AutoMapper +{ + public class ResourceClaimResolver : IValueResolver + { + public ResourceClaim? Resolve(IResourceClaimOnClaimSetRequest source, EditResourceOnClaimSetModel destination, ResourceClaim? destMember, ResolutionContext context) + { + return new ResourceClaim + { + Id = source.ResourceClaimId, + Actions = source.ResourceClaimActions + }; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/CommonQueryParams.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/CommonQueryParams.cs deleted file mode 100644 index f87659b04..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/CommonQueryParams.cs +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using Microsoft.AspNetCore.Mvc; - -namespace EdFi.Ods.AdminApi.Infrastructure; -public struct CommonQueryParams -{ - [FromQuery(Name = "offset")] - public int? Offset { get; set; } - [FromQuery(Name = "limit")] - public int? Limit { get; set; } - public CommonQueryParams() { } - public CommonQueryParams(int? offset, int? limit) - { - Offset = offset; - Limit = limit; - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddApiClientCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddApiClientCommand.cs new file mode 100644 index 000000000..1adba6ba5 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddApiClientCommand.cs @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Settings; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IAddApiClientCommand +{ + AddApiClientResult Execute(IAddApiClientModel apiClientModel, IOptions options); +} + +public class AddApiClientCommand(IUsersContext usersContext) : IAddApiClientCommand +{ + private readonly IUsersContext _usersContext = usersContext; + + public AddApiClientResult Execute(IAddApiClientModel apiClientModel, IOptions options) + { + var application = _usersContext.Applications + .Include(a => a.Vendor) + .Single(a => a.ApplicationId == apiClientModel.ApplicationId); + + var odsInstances = apiClientModel.OdsInstanceIds != null + ? _usersContext.OdsInstances.Where(o => apiClientModel.OdsInstanceIds.Contains(o.OdsInstanceId)) + : null; + + var applicationEdOrgs = application.EducationOrganizationIds(); + + var apiClient = new ApiClient(true) + { + Name = apiClientModel.Name, + IsApproved = true, + Application = application, + UseSandbox = false, + KeyStatus = "Active", + User = application.Vendor.Users.FirstOrDefault(), + ApplicationEducationOrganizations = applicationEdOrgs?.Select(eu => new ApplicationEducationOrganization + { + EducationOrganizationId = eu, + Application = application, + }).ToList(), + }; + + _usersContext.ApiClients.Add(apiClient); + + if (odsInstances != null && odsInstances.Any()) + { + foreach (var odsInstance in odsInstances) + { + _usersContext.ApiClientOdsInstances.Add(new ApiClientOdsInstance + { + OdsInstance = odsInstance, + ApiClient = apiClient, + }); + } + } + + _usersContext.SaveChanges(); + + return new AddApiClientResult + { + Id = apiClient.ApiClientId, + Name = apiClient.Name, + ApplicationId = application.ApplicationId, + Key = apiClient.Key, + Secret = apiClient.Secret, + }; + } +} + +public interface IAddApiClientModel +{ + string Name { get; } + bool IsApproved { get; } + int ApplicationId { get; } + IEnumerable? OdsInstanceIds { get; } +} +public class AddApiClientResult +{ + public int Id { get; set; } + public int ApplicationId { get; set; } + public string Name { get; set; } = string.Empty; + public string Key { get; set; } = string.Empty; + public string Secret { get; set; } = string.Empty; +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddApiClientOdsInstanceCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddApiClientOdsInstanceCommand.cs new file mode 100644 index 000000000..c42b26992 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddApiClientOdsInstanceCommand.cs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IAddApiClientOdsInstanceCommand +{ + ApiClientOdsInstance Execute(ApiClientOdsInstance newApiClientOdsInstance); +} + +public class AddApiClientOdsInstanceCommand : IAddApiClientOdsInstanceCommand +{ + private readonly IUsersContext _context; + + public AddApiClientOdsInstanceCommand(IUsersContext context) + { + _context = context; + } + + public ApiClientOdsInstance Execute(ApiClientOdsInstance newApiClientOdsInstance) + { + + var apiClientOdsInstance = new ApiClientOdsInstance + { + ApiClient = newApiClientOdsInstance.ApiClient, + OdsInstance = newApiClientOdsInstance.OdsInstance + }; + _context.ApiClientOdsInstances.Add(apiClientOdsInstance); + _context.SaveChanges(); + return apiClientOdsInstance; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddApplicationCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddApplicationCommand.cs index 7af49d1cb..22ba8241b 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddApplicationCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddApplicationCommand.cs @@ -1,112 +1,136 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Admin.DataAccess.Contexts; -using EdFi.Admin.DataAccess.Models; -using Microsoft.EntityFrameworkCore; - -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; - -public interface IAddApplicationCommand -{ - AddApplicationResult Execute(IAddApplicationModel applicationModel); -} - -public class AddApplicationCommand : IAddApplicationCommand -{ - private readonly IUsersContext _usersContext; - private readonly InstanceContext _instanceContext; - - public AddApplicationCommand(IUsersContext usersContext, InstanceContext instanceContext) - { - _usersContext = usersContext; - _instanceContext = instanceContext; - } - - public AddApplicationResult Execute(IAddApplicationModel applicationModel) - { - var profile = applicationModel.ProfileId.HasValue - ? _usersContext.Profiles.SingleOrDefault(p => p.ProfileId == applicationModel.ProfileId.Value) - : null; - - var vendor = _usersContext.Vendors.Include(x => x.Users) - .Single(v => v.VendorId == applicationModel.VendorId); - - OdsInstance? odsInstance; - - if (_instanceContext != null && !string.IsNullOrEmpty(_instanceContext.Name)) - { - odsInstance = _usersContext.OdsInstances.AsEnumerable().FirstOrDefault(x => - x.Name.Equals(_instanceContext.Name, StringComparison.InvariantCultureIgnoreCase)); - } - else - { - odsInstance = _usersContext.OdsInstances.FirstOrDefault(o => o.OdsInstanceId == applicationModel.OdsInstanceId); - } - - var user = vendor.Users.FirstOrDefault(); - - var apiClient = new ApiClient(true) - { - Name = applicationModel.ApplicationName, - IsApproved = true, - UseSandbox = false, - KeyStatus = "Active", - User = user - }; - - var applicationEdOrgs = applicationModel.EducationOrganizationIds == null - ? Enumerable.Empty() - : applicationModel.EducationOrganizationIds.Select(id => new ApplicationEducationOrganization - { - Clients = new List { apiClient }, - EducationOrganizationId = id - }); - - var application = new Application - { - ApplicationName = applicationModel.ApplicationName, - ApiClients = new List { apiClient }, - ApplicationEducationOrganizations = new List(applicationEdOrgs), - ClaimSetName = applicationModel.ClaimSetName, - Profiles = new List(), - Vendor = vendor, - OperationalContextUri = OperationalContext.DefaultOperationalContextUri, - OdsInstance = odsInstance - }; - - if (profile != null) - { - application.Profiles.Add(profile); - } - - _usersContext.Applications.Add(application); - _usersContext.SaveChanges(); - - return new AddApplicationResult - { - ApplicationId = application.ApplicationId, - Key = apiClient.Key, - Secret = apiClient.Secret - }; - } -} - -public interface IAddApplicationModel -{ - string? ApplicationName { get; } - int VendorId { get; } - string? ClaimSetName { get; } - int? ProfileId { get; } - int? OdsInstanceId { get; } - IEnumerable? EducationOrganizationIds { get; } -} - -public class AddApplicationResult -{ - public int ApplicationId { get; set; } - public string? Key { get; set; } - public string? Secret { get; set; } -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IAddApplicationCommand +{ + AddApplicationResult Execute(IAddApplicationModel applicationModel, IOptions options); +} + +public class AddApplicationCommand : IAddApplicationCommand +{ + private readonly IUsersContext _usersContext; + + public AddApplicationCommand(IUsersContext usersContext) + { + _usersContext = usersContext; + } + + public AddApplicationResult Execute(IAddApplicationModel applicationModel, IOptions options) + { + if (options.Value.PreventDuplicateApplications) + { + ValidateApplicationExistsQuery validateApplicationExists = new ValidateApplicationExistsQuery(_usersContext); + bool applicationExists = validateApplicationExists.Execute(applicationModel); + if (applicationExists) + { + var adminApiException = new AdminApiException("The Application already exists"); + adminApiException.StatusCode = HttpStatusCode.Conflict; + throw adminApiException; + } + } + var profiles = applicationModel.ProfileIds != null + ? _usersContext.Profiles.Where(p => applicationModel.ProfileIds!.Contains(p.ProfileId)) + : null; + + var vendor = _usersContext.Vendors.Include(x => x.Users) + .Single(v => v.VendorId == applicationModel.VendorId); + + var odsInstances = applicationModel.OdsInstanceIds != null + ? _usersContext.OdsInstances.Where(o => applicationModel.OdsInstanceIds.Contains(o.OdsInstanceId)) + : null; + + var user = vendor.Users.FirstOrDefault(); + + var apiClient = new ApiClient(true) + { + Name = applicationModel.ApplicationName, + IsApproved = applicationModel.Enabled ?? true, + UseSandbox = false, + KeyStatus = "Active", + User = user, + }; + + var applicationEdOrgs = applicationModel.EducationOrganizationIds == null + ? Enumerable.Empty() + : applicationModel.EducationOrganizationIds.Select(id => new ApplicationEducationOrganization + { + ApiClients = new List { apiClient }, + EducationOrganizationId = id + }); + + var application = new Application + { + ApplicationName = applicationModel.ApplicationName, + ApiClients = new List { apiClient }, + ApplicationEducationOrganizations = new List(applicationEdOrgs), + ClaimSetName = applicationModel.ClaimSetName, + Profiles = new List(), + Vendor = vendor, + OperationalContextUri = OperationalContext.DefaultOperationalContextUri + }; + + if (profiles != null) + { + foreach (var profile in profiles) + { + application.Profiles.Add(profile); + } + } + + _usersContext.Applications.Add(application); + + if (odsInstances != null && odsInstances.Count() > 0) + { + foreach (var odsInstance in odsInstances) + { + _usersContext.ApiClientOdsInstances.Add(new ApiClientOdsInstance + { + OdsInstance = odsInstance, + ApiClient = apiClient, + }); + } + } + + _usersContext.SaveChanges(); + + return new AddApplicationResult + { + ApplicationId = application.ApplicationId, + Key = apiClient.Key, + Secret = apiClient.Secret + }; + } +} + +public interface IAddApplicationModel +{ + string? ApplicationName { get; } + int VendorId { get; } + string? ClaimSetName { get; } + IEnumerable? ProfileIds { get; } + IEnumerable? EducationOrganizationIds { get; } + IEnumerable? OdsInstanceIds { get; } + bool? Enabled { get; } +} + +public class AddApplicationResult +{ + public int ApplicationId { get; set; } + public string? Key { get; set; } + public string? Secret { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceCommand.cs index 759698334..139384dab 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceCommand.cs @@ -1,48 +1,44 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Admin.DataAccess.Contexts; -using EdFi.Admin.DataAccess.Models; - -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; - -public interface IAddOdsInstanceCommand -{ - OdsInstance Execute(IAddOdsInstanceModel newOdsInstance); -} - -public class AddOdsInstanceCommand : IAddOdsInstanceCommand -{ - private readonly IUsersContext _context; - - public AddOdsInstanceCommand(IUsersContext context) - { - _context = context; - } - - public OdsInstance Execute(IAddOdsInstanceModel newOdsInstance) - { - var odsInstance = new OdsInstance - { - Name = newOdsInstance.Name, - InstanceType = newOdsInstance.InstanceType, - Status = newOdsInstance.Status, - IsExtended = newOdsInstance.IsExtended ?? false, - Version = newOdsInstance.Version, - }; - _context.OdsInstances.Add(odsInstance); - _context.SaveChanges(); - return odsInstance; - } -} - -public interface IAddOdsInstanceModel -{ - string? Name { get; } - string? InstanceType { get; } - string? Status { get; set; } - bool? IsExtended { get; } - string? Version { get; set; } -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IAddOdsInstanceCommand +{ + OdsInstance Execute(IAddOdsInstanceModel newOdsInstance); +} + +public class AddOdsInstanceCommand : IAddOdsInstanceCommand +{ + private readonly IUsersContext _context; + + public AddOdsInstanceCommand(IUsersContext context) + { + _context = context; + } + + public OdsInstance Execute(IAddOdsInstanceModel newOdsInstance) + { + var odsInstance = new OdsInstance + { + Name = newOdsInstance.Name, + InstanceType = newOdsInstance.InstanceType, + ConnectionString = newOdsInstance.ConnectionString + }; + _context.OdsInstances.Add(odsInstance); + _context.SaveChanges(); + return odsInstance; + } +} + +public interface IAddOdsInstanceModel +{ + string? Name { get; } + string? InstanceType { get; } + string? ConnectionString { get; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceContextCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceContextCommand.cs new file mode 100644 index 000000000..7af2cebe4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceContextCommand.cs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IAddOdsInstanceContextCommand +{ + OdsInstanceContext Execute(IAddOdsInstanceContextModel newOdsInstanceContext); +} + +public class AddOdsInstanceContextCommand : IAddOdsInstanceContextCommand +{ + private readonly IUsersContext _context; + + public AddOdsInstanceContextCommand(IUsersContext context) + { + _context = context; + } + + public OdsInstanceContext Execute(IAddOdsInstanceContextModel newOdsInstanceContext) + { + var odsInstance = _context.OdsInstances.SingleOrDefault(v => v.OdsInstanceId == newOdsInstanceContext.OdsInstanceId) ?? + throw new NotFoundException("odsInstance", newOdsInstanceContext.OdsInstanceId); + + var odsInstanceContext = new OdsInstanceContext + { + ContextKey = newOdsInstanceContext.ContextKey, + ContextValue = newOdsInstanceContext.ContextValue, + OdsInstance = odsInstance + }; + _context.OdsInstanceContexts.Add(odsInstanceContext); + _context.SaveChanges(); + return odsInstanceContext; + } +} + +public interface IAddOdsInstanceContextModel +{ + public int OdsInstanceId { get; set; } + public string? ContextKey { get; set; } + public string? ContextValue { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceDerivativeCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceDerivativeCommand.cs new file mode 100644 index 000000000..e47e0eff4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceDerivativeCommand.cs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IAddOdsInstanceDerivativeCommand +{ + OdsInstanceDerivative Execute(IAddOdsInstanceDerivativeModel newOdsInstanceDerivative); +} + +public class AddOdsInstanceDerivativeCommand : IAddOdsInstanceDerivativeCommand +{ + private readonly IUsersContext _context; + + public AddOdsInstanceDerivativeCommand(IUsersContext context) + { + _context = context; + } + + public OdsInstanceDerivative Execute(IAddOdsInstanceDerivativeModel newOdsInstanceDerivative) + { + var odsInstance = _context.OdsInstances.SingleOrDefault(v => v.OdsInstanceId == newOdsInstanceDerivative.OdsInstanceId) ?? + throw new NotFoundException("odsInstance", newOdsInstanceDerivative.OdsInstanceId); + + var odsInstanceDerivative = new OdsInstanceDerivative + { + ConnectionString = newOdsInstanceDerivative.ConnectionString, + DerivativeType = newOdsInstanceDerivative.DerivativeType, + OdsInstance = odsInstance + }; + _context.OdsInstanceDerivatives.Add(odsInstanceDerivative); + _context.SaveChanges(); + return odsInstanceDerivative; + } +} + +public interface IAddOdsInstanceDerivativeModel +{ + public int OdsInstanceId { get; set; } + public string? DerivativeType { get; set; } + public string? ConnectionString { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddProfileCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddProfileCommand.cs new file mode 100644 index 000000000..0e9a6780c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddProfileCommand.cs @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IAddProfileCommand +{ + Profile Execute(IAddProfileModel newProfile); +} + +public class AddProfileCommand : IAddProfileCommand +{ + private readonly IUsersContext _context; + + public AddProfileCommand(IUsersContext context) + { + _context = context; + } + + public Profile Execute(IAddProfileModel newProfile) + { + var profile = new Profile + { + ProfileName = newProfile.Name, + ProfileDefinition = newProfile.Definition + }; + _context.Profiles.Add(profile); + _context.SaveChanges(); + return profile; + } +} + +public interface IAddProfileModel +{ + string? Name { get; } + string? Definition { get; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddVendorCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddVendorCommand.cs index 8049d701d..6b7266f49 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddVendorCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddVendorCommand.cs @@ -3,8 +3,6 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System.Collections.Generic; -using System.Linq; using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; using VendorUser = EdFi.Admin.DataAccess.Models.User; diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteApiClientCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteApiClientCommand.cs new file mode 100644 index 000000000..f796077f1 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteApiClientCommand.cs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IDeleteApiClientCommand +{ + void Execute(int id); +} + +public class DeleteApiClientCommand : IDeleteApiClientCommand +{ + private readonly IUsersContext _context; + + public DeleteApiClientCommand(IUsersContext context) + { + _context = context; + } + + public void Execute(int id) + { + var apiClient = _context.ApiClients + .SingleOrDefault(a => a.ApiClientId == id) ?? throw new NotFoundException("apiclient", id); + + var currentClientAccessTokens = _context.ClientAccessTokens.Where(o => apiClient.ApiClientId.Equals(o.ApiClient.ApiClientId)); + + if (currentClientAccessTokens.Any()) + { + _context.ClientAccessTokens.RemoveRange(currentClientAccessTokens); + } + + var currentApiClientOdsInstances = _context.ApiClientOdsInstances.Where(o => apiClient.ApiClientId.Equals(o.ApiClient.ApiClientId)); + + if (currentApiClientOdsInstances.Any()) + { + _context.ApiClientOdsInstances.RemoveRange(currentApiClientOdsInstances); + } + + _context.ApiClients.Remove(apiClient); + _context.SaveChanges(); + } + +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteApplicationCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteApplicationCommand.cs index 254ba5f0d..230166925 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteApplicationCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteApplicationCommand.cs @@ -3,13 +3,11 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System; -using System.Linq; using EdFi.Admin.DataAccess.Contexts; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using Microsoft.EntityFrameworkCore; - +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; public interface IDeleteApplicationCommand @@ -29,13 +27,14 @@ public DeleteApplicationCommand(IUsersContext context) public void Execute(int id) { var application = _context.Applications - .Include(a => a.ApiClients).ThenInclude(c => c.ClientAccessTokens) + .Include(a => a.ApiClients) .Include(a => a.ApplicationEducationOrganizations) + .Include(a => a.Profiles) .SingleOrDefault(a => a.ApplicationId == id) ?? throw new NotFoundException("application", id); if (application != null && application.Vendor.IsSystemReservedVendor()) { - throw new Exception("This Application is required for proper system function and may not be modified"); + throw new ArgumentException("This Application is required for proper system function and may not be modified"); } if (application == null) @@ -43,15 +42,46 @@ public void Execute(int id) return; } - application.ApiClients.ToList().ForEach(a => - { - a.ClientAccessTokens.ToList().ForEach(t => _context.ClientAccessTokens.Remove(t)); - _context.Clients.Remove(a); - }); + var currentClientAccessTokens = _context.ClientAccessTokens.Where(o => application.ApiClients.Contains(o.ApiClient)); + + if (currentClientAccessTokens.Any()) + { + _context.ClientAccessTokens.RemoveRange(currentClientAccessTokens); + } - application.ApplicationEducationOrganizations.ToList().ForEach(o => _context.ApplicationEducationOrganizations.Remove(o)); + var currentApiClientOdsInstances = _context.ApiClientOdsInstances.Where(o => application.ApiClients.Contains(o.ApiClient)); + + if (currentApiClientOdsInstances.Any()) + { + _context.ApiClientOdsInstances.RemoveRange(currentApiClientOdsInstances); + } + + var currentApplicationEducationOrganizations = _context.ApplicationEducationOrganizations.Where(aeo => aeo.Application.ApplicationId == application.ApplicationId); + + if (currentApplicationEducationOrganizations.Any()) + { + _context.ApplicationEducationOrganizations.RemoveRange(currentApplicationEducationOrganizations); + } + + var currentApplicationClients = _context.ApiClients.AsEnumerable().Where(o => application.ApiClients.AsEnumerable().Any(oapp => oapp.ApiClientId == o.ApiClientId)); + + if (currentApplicationClients.Any()) + { + _context.ApiClients.RemoveRange(currentApplicationClients); + } + + var currentProfiles = application.Profiles.ToList(); + + if (currentProfiles.Any()) + { + foreach (var profile in currentProfiles) + { + application.Profiles.Remove(profile); + } + } _context.Applications.Remove(application); _context.SaveChanges(); } + } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceCommand.cs index 9fc8d5669..f4e2cee6c 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceCommand.cs @@ -1,30 +1,31 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Admin.DataAccess.Contexts; - -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; - -public interface IDeleteOdsInstanceCommand -{ - void Execute(int id); -} - -public class DeleteOdsInstanceCommand : IDeleteOdsInstanceCommand -{ - private readonly IUsersContext _context; - - public DeleteOdsInstanceCommand(IUsersContext context) - { - _context = context; - } - - public void Execute(int id) - { - var odsInstance = _context.OdsInstances.SingleOrDefault(v => v.OdsInstanceId == id) ?? throw new NotFoundException("odsInstance", id); - _context.OdsInstances.Remove(odsInstance); - _context.SaveChanges(); - } -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IDeleteOdsInstanceCommand +{ + void Execute(int id); +} + +public class DeleteOdsInstanceCommand : IDeleteOdsInstanceCommand +{ + private readonly IUsersContext _context; + + public DeleteOdsInstanceCommand(IUsersContext context) + { + _context = context; + } + + public void Execute(int id) + { + var odsInstance = _context.OdsInstances.SingleOrDefault(v => v.OdsInstanceId == id) ?? throw new NotFoundException("odsInstance", id); + _context.OdsInstances.Remove(odsInstance); + _context.SaveChanges(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceContextCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceContextCommand.cs new file mode 100644 index 000000000..d37d7d2cd --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceContextCommand.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IDeleteOdsInstanceContextCommand +{ + void Execute(int id); +} + +public class DeleteOdsInstanceContextCommand : IDeleteOdsInstanceContextCommand +{ + private readonly IUsersContext _context; + + public DeleteOdsInstanceContextCommand(IUsersContext context) + { + _context = context; + } + + public void Execute(int id) + { + var odsInstanceContext = _context.OdsInstanceContexts.SingleOrDefault(v => v.OdsInstanceContextId == id) ?? throw new NotFoundException("odsInstanceContext", id); + _context.OdsInstanceContexts.Remove(odsInstanceContext); + _context.SaveChanges(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceDerivativeCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceDerivativeCommand.cs new file mode 100644 index 000000000..4cbf3b2a2 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceDerivativeCommand.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IDeleteOdsInstanceDerivativeCommand +{ + void Execute(int id); +} + +public class DeleteOdsInstanceDerivativeCommand : IDeleteOdsInstanceDerivativeCommand +{ + private readonly IUsersContext _context; + + public DeleteOdsInstanceDerivativeCommand(IUsersContext context) + { + _context = context; + } + + public void Execute(int id) + { + var odsInstanceDerivative = _context.OdsInstanceDerivatives.SingleOrDefault(v => v.OdsInstanceDerivativeId == id) ?? throw new NotFoundException("odsInstanceDerivative", id); + _context.OdsInstanceDerivatives.Remove(odsInstanceDerivative); + _context.SaveChanges(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteProfileCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteProfileCommand.cs new file mode 100644 index 000000000..dc259db35 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteProfileCommand.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IDeleteProfileCommand +{ + void Execute(int id); +} + +public class DeleteProfileCommand : IDeleteProfileCommand +{ + private readonly IUsersContext _context; + + public DeleteProfileCommand(IUsersContext context) + { + _context = context; + } + + public void Execute(int id) + { + var profile = _context.Profiles.SingleOrDefault(v => v.ProfileId == id) ?? throw new NotFoundException("profile", id); + _context.Profiles.Remove(profile); + _context.SaveChanges(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteVendorCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteVendorCommand.cs index 6bd72a3cf..cf2aa567d 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteVendorCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteVendorCommand.cs @@ -3,11 +3,9 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System; -using System.Linq; using EdFi.Admin.DataAccess.Contexts; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; using Microsoft.EntityFrameworkCore; namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; @@ -26,13 +24,14 @@ public DeleteVendorCommand(IUsersContext context, IDeleteApplicationCommand dele public void Execute(int id) { var vendor = _context.Vendors - .Include(x => x.Applications) - .Include(x => x.Users) + .Include(v => v.Applications) + .Include(v => v.VendorNamespacePrefixes) + .Include(v => v.Users) .SingleOrDefault(v => v.VendorId == id) ?? throw new NotFoundException("vendor", id); if (vendor.IsSystemReservedVendor()) { - throw new Exception("This Vendor is required for proper system function and may not be deleted"); + throw new ArgumentException("This Vendor is required for proper system function and may not be deleted"); } foreach (var application in vendor.Applications.ToList()) @@ -42,6 +41,16 @@ public void Execute(int id) foreach (var user in vendor.Users.ToList()) { + if (_context.ApiClients.Any()) + { + var apiClient = + _context.ApiClients + .AsEnumerable().SingleOrDefault(o => o.User?.UserId == user?.UserId); + if (apiClient != null) + { + _context.ApiClients.Remove(apiClient); + } + } _context.Users.Remove(user); } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditApiClientCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditApiClientCommand.cs new file mode 100644 index 000000000..ab5e43e0f --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditApiClientCommand.cs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IEditApiClientCommand +{ + ApiClient Execute(IEditApiClientModel model); +} + +public class EditApiClientCommand : IEditApiClientCommand +{ + private readonly IUsersContext _context; + + public EditApiClientCommand(IUsersContext context) + { + _context = context; + } + + public ApiClient Execute(IEditApiClientModel model) + { + var apiClient = _context.ApiClients + .SingleOrDefault(a => a.ApiClientId == model.Id) ?? throw new NotFoundException("apiclient", model.Id); + + var newOdsInstances = model.OdsInstanceIds != null + ? _context.OdsInstances.Where(p => model.OdsInstanceIds.Contains(p.OdsInstanceId)) + : null; + + var currentApiClientId = apiClient.ApiClientId; + apiClient.Name = model.Name; + apiClient.IsApproved = model.IsApproved; + + _context.ApiClientOdsInstances.RemoveRange(_context.ApiClientOdsInstances.Where(o => o.ApiClient.ApiClientId == currentApiClientId)); + + if (newOdsInstances != null) + { + foreach (var newOdsInstance in newOdsInstances) + { + _context.ApiClientOdsInstances.Add(new ApiClientOdsInstance { ApiClient = apiClient, OdsInstance = newOdsInstance }); + } + } + + _context.SaveChanges(); + return apiClient; + } +} + +public interface IEditApiClientModel +{ + int Id { get; } + string Name { get; } + bool IsApproved { get; } + int ApplicationId { get; } + IEnumerable? OdsInstanceIds { get; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditApplicationCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditApplicationCommand.cs index 09544c9c7..03c1fec1e 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditApplicationCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditApplicationCommand.cs @@ -2,15 +2,13 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; -using System.Collections.ObjectModel; using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; -using EdFi.Common.Utils.Extensions; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using Microsoft.EntityFrameworkCore; - +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; public interface IEditApplicationCommand @@ -30,44 +28,76 @@ public EditApplicationCommand(IUsersContext context) public Application Execute(IEditApplicationModel model) { var application = _context.Applications - .Include(a => a.Vendor) .Include(a => a.ApplicationEducationOrganizations) - .Include(a => a.ApiClients) .Include(a => a.Profiles) - .SingleOrDefault(a => a.ApplicationId == model.ApplicationId) ?? throw new NotFoundException("application", model.ApplicationId); + .Include(a => a.Vendor) + .Include(a => a.ApiClients) + .SingleOrDefault(a => a.ApplicationId == model.Id) ?? throw new NotFoundException("application", model.Id); if (application.Vendor.IsSystemReservedVendor()) { - throw new Exception("This Application is required for proper system function and may not be modified"); + throw new AdminApiException("This Application is required for proper system function and may not be modified"); } var newVendor = _context.Vendors.Single(v => v.VendorId == model.VendorId); - var newProfile = model.ProfileId.HasValue - ? _context.Profiles.Single(p => p.ProfileId == model.ProfileId.Value) + var newProfiles = model.ProfileIds != null + ? _context.Profiles.Where(p => model.ProfileIds.Contains(p.ProfileId)) + : null; + var newOdsInstances = model.OdsInstanceIds != null + ? _context.OdsInstances.Where(p => model.OdsInstanceIds.Contains(p.OdsInstanceId)) : null; var apiClient = application.ApiClients.Single(); + var currentApiClientId = apiClient.ApiClientId; apiClient.Name = model.ApplicationName; + apiClient.IsApproved = model.Enabled ?? true; + + _context.ApiClientOdsInstances.RemoveRange(_context.ApiClientOdsInstances.Where(o => o.ApiClient.ApiClientId == currentApiClientId)); + _context.ApplicationEducationOrganizations.RemoveRange(_context.ApplicationEducationOrganizations.Where(aeo => aeo.Application.ApplicationId == application.ApplicationId)); + + var currentProfiles = application.Profiles.ToList(); + foreach (var profile in currentProfiles) + { + application.Profiles.Remove(profile); + } + application.ApplicationName = model.ApplicationName; application.ClaimSetName = model.ClaimSetName; application.Vendor = newVendor; - application.ApplicationEducationOrganizations ??= new Collection(); + var newApplicationEdOrgs = model.EducationOrganizationIds == null + ? [] + : model.EducationOrganizationIds.Select(id => new ApplicationEducationOrganization + { + ApiClients = new List { apiClient }, + EducationOrganizationId = id, + Application = application, + }); - // Quick and dirty: simply remove all existing links to ApplicationEducationOrganizations... - application.ApplicationEducationOrganizations.ToList().ForEach(x => _context.ApplicationEducationOrganizations.Remove(x)); - application.ApplicationEducationOrganizations.Clear(); - // ... and now create the new proper list. - model.EducationOrganizationIds?.ForEach(id => application.ApplicationEducationOrganizations.Add(application.CreateApplicationEducationOrganization(id))); + foreach (var appEdOrg in newApplicationEdOrgs) + { + application.ApplicationEducationOrganizations.Add(appEdOrg); + } - application.Profiles ??= new Collection(); + application.Profiles ??= []; application.Profiles.Clear(); - if (newProfile != null) + if (newProfiles != null) + { + foreach (var profile in newProfiles) + { + application.Profiles.Add(profile); + } + } + + if (newOdsInstances != null) { - application.Profiles.Add(newProfile); + foreach (var newOdsInstance in newOdsInstances) + { + _context.ApiClientOdsInstances.Add(new ApiClientOdsInstance { ApiClient = apiClient, OdsInstance = newOdsInstance }); + } } _context.SaveChanges(); @@ -77,11 +107,12 @@ public Application Execute(IEditApplicationModel model) public interface IEditApplicationModel { - int ApplicationId { get; } + int Id { get; } string? ApplicationName { get; } int VendorId { get; } string? ClaimSetName { get; } - int? ProfileId { get; } - int? OdsInstanceId { get; } - IEnumerable? EducationOrganizationIds { get; } + IEnumerable? ProfileIds { get; } + IEnumerable? EducationOrganizationIds { get; } + IEnumerable? OdsInstanceIds { get; } + bool? Enabled { get; } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditOdsInstanceCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditOdsInstanceCommand.cs index 62009893b..cb2496d4b 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditOdsInstanceCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditOdsInstanceCommand.cs @@ -1,50 +1,49 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Admin.DataAccess.Contexts; -using EdFi.Admin.DataAccess.Models; - -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; - -public interface IEditOdsInstanceCommand -{ - OdsInstance Execute(IEditOdsInstanceModel changedOdsInstanceData); -} - -public class EditOdsInstanceCommand : IEditOdsInstanceCommand -{ - private readonly IUsersContext _context; - - public EditOdsInstanceCommand(IUsersContext context) - { - _context = context; - } - - public OdsInstance Execute(IEditOdsInstanceModel changedOdsInstanceData) - { - var odsInstance = _context.OdsInstances.SingleOrDefault(v => v.OdsInstanceId == changedOdsInstanceData.OdsInstanceId) ?? - throw new NotFoundException("odsInstance", changedOdsInstanceData.OdsInstanceId); - - odsInstance.Name = changedOdsInstanceData.Name; - odsInstance.InstanceType = changedOdsInstanceData.InstanceType; - odsInstance.IsExtended = changedOdsInstanceData.IsExtended ?? false; - odsInstance.Status = changedOdsInstanceData.Status; - odsInstance.Version = changedOdsInstanceData.Version; - - _context.SaveChanges(); - return odsInstance; - } -} - -public interface IEditOdsInstanceModel -{ - public int OdsInstanceId { get; set; } - string? Name { get; } - string? InstanceType { get; } - string? Status { get; set; } - bool? IsExtended { get; } - string? Version { get; set; } -} - + +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IEditOdsInstanceCommand +{ + OdsInstance Execute(IEditOdsInstanceModel changedOdsInstanceData); +} + +public class EditOdsInstanceCommand : IEditOdsInstanceCommand +{ + private readonly IUsersContext _context; + + public EditOdsInstanceCommand(IUsersContext context) + { + _context = context; + } + + public OdsInstance Execute(IEditOdsInstanceModel changedOdsInstanceData) + { + var odsInstance = _context.OdsInstances.SingleOrDefault(v => v.OdsInstanceId == changedOdsInstanceData.Id) ?? + throw new NotFoundException("odsInstance", changedOdsInstanceData.Id); + + odsInstance.Name = changedOdsInstanceData.Name; + odsInstance.InstanceType = changedOdsInstanceData.InstanceType; + if (!string.IsNullOrEmpty(changedOdsInstanceData.ConnectionString)) + odsInstance.ConnectionString = changedOdsInstanceData.ConnectionString; + + _context.SaveChanges(); + return odsInstance; + } +} + +public interface IEditOdsInstanceModel +{ + public int Id { get; set; } + string? Name { get; } + string? InstanceType { get; } + string? ConnectionString { get; } +} + diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditOdsInstanceContextCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditOdsInstanceContextCommand.cs new file mode 100644 index 000000000..2fa77e3b5 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditOdsInstanceContextCommand.cs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IEditOdsInstanceContextCommand +{ + OdsInstanceContext Execute(IEditOdsInstanceContextModel changedOdsInstanceContextData); +} + +public class EditOdsInstanceContextCommand : IEditOdsInstanceContextCommand +{ + private readonly IUsersContext _context; + + public EditOdsInstanceContextCommand(IUsersContext context) + { + _context = context; + } + + public OdsInstanceContext Execute(IEditOdsInstanceContextModel changedOdsInstanceContextData) + { + var odsInstanceContext = _context.OdsInstanceContexts + .Include(oid => oid.OdsInstance) + .SingleOrDefault(v => v.OdsInstanceContextId == changedOdsInstanceContextData.Id) ?? + throw new NotFoundException("odsInstanceContext", changedOdsInstanceContextData.Id); + var odsInstance = _context.OdsInstances.SingleOrDefault(v => v.OdsInstanceId == changedOdsInstanceContextData.OdsInstanceId) ?? + throw new NotFoundException("odsInstance", changedOdsInstanceContextData.OdsInstanceId); + + odsInstanceContext.ContextKey = changedOdsInstanceContextData.ContextKey; + odsInstanceContext.OdsInstance = odsInstance; + odsInstanceContext.ContextValue = changedOdsInstanceContextData.ContextValue; + + _context.SaveChanges(); + return odsInstanceContext; + } +} + +public interface IEditOdsInstanceContextModel +{ + public int Id { get; set; } + public int OdsInstanceId { get; set; } + public string? ContextKey { get; set; } + public string? ContextValue { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditOdsInstanceDerivativeCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditOdsInstanceDerivativeCommand.cs new file mode 100644 index 000000000..4c178b9f3 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditOdsInstanceDerivativeCommand.cs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IEditOdsInstanceDerivativeCommand +{ + OdsInstanceDerivative Execute(IEditOdsInstanceDerivativeModel changedOdsInstanceDerivativeData); +} + +public class EditOdsInstanceDerivativeCommand : IEditOdsInstanceDerivativeCommand +{ + private readonly IUsersContext _context; + + public EditOdsInstanceDerivativeCommand(IUsersContext context) + { + _context = context; + } + + public OdsInstanceDerivative Execute(IEditOdsInstanceDerivativeModel changedOdsInstanceDerivativeData) + { + var odsInstance = _context.OdsInstances + .SingleOrDefault(v => v.OdsInstanceId == changedOdsInstanceDerivativeData.OdsInstanceId) ?? + throw new NotFoundException("odsInstance", changedOdsInstanceDerivativeData.OdsInstanceId); + var odsInstanceDerivative = _context.OdsInstanceDerivatives + .Include(oid => oid.OdsInstance) + .SingleOrDefault(v => v.OdsInstanceDerivativeId == changedOdsInstanceDerivativeData.Id) ?? + throw new NotFoundException("odsInstanceDerivative", changedOdsInstanceDerivativeData.Id); + + odsInstanceDerivative.DerivativeType = changedOdsInstanceDerivativeData.DerivativeType; + odsInstanceDerivative.OdsInstance = odsInstance; + odsInstanceDerivative.ConnectionString = changedOdsInstanceDerivativeData.ConnectionString; + + _context.SaveChanges(); + return odsInstanceDerivative; + } +} + +public interface IEditOdsInstanceDerivativeModel +{ + public int Id { get; set; } + public int OdsInstanceId { get; set; } + public string? DerivativeType { get; set; } + public string? ConnectionString { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditProfileCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditProfileCommand.cs new file mode 100644 index 000000000..5bdfef887 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditProfileCommand.cs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IEditProfileCommand +{ + Profile Execute(IEditProfileModel changedProfileData); +} + +public class EditProfileCommand : IEditProfileCommand +{ + private readonly IUsersContext _context; + + public EditProfileCommand(IUsersContext context) + { + _context = context; + } + + public Profile Execute(IEditProfileModel changedProfileData) + { + var profile = _context.Profiles.SingleOrDefault(v => v.ProfileId == changedProfileData.Id) ?? + throw new NotFoundException("profile", changedProfileData.Id); + + profile.ProfileName = changedProfileData.Name; + profile.ProfileDefinition = changedProfileData.Definition; + + _context.SaveChanges(); + return profile; + } +} + +public interface IEditProfileModel +{ + public int Id { get; set; } + string? Name { get; } + string? Definition { get; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditVendorCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditVendorCommand.cs index a7b7798a7..6b023c2d9 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditVendorCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/EditVendorCommand.cs @@ -6,6 +6,7 @@ using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; using Microsoft.EntityFrameworkCore; using VendorUser = EdFi.Admin.DataAccess.Models.User; @@ -23,13 +24,13 @@ public EditVendorCommand(IUsersContext context) public Vendor Execute(IEditVendor changedVendorData) { var vendor = _context.Vendors - .Include(x => x.VendorNamespacePrefixes) - .Include(x => x.Users) - .SingleOrDefault(v => v.VendorId == changedVendorData.VendorId) ?? throw new NotFoundException("vendor", changedVendorData.VendorId); + .Include(v => v.VendorNamespacePrefixes) + .Include(v => v.Users) + .SingleOrDefault(v => v.VendorId == changedVendorData.Id) ?? throw new NotFoundException("vendor", changedVendorData.Id); if (vendor.IsSystemReservedVendor()) { - throw new Exception("This vendor is required for proper system function and may not be modified."); + throw new ArgumentException("This vendor is required for proper system function and may not be modified."); } vendor.VendorName = changedVendorData.Company; @@ -79,7 +80,7 @@ public Vendor Execute(IEditVendor changedVendorData) public interface IEditVendor { - int VendorId { get; set; } + int Id { get; set; } string? Company { get; set; } string? NamespacePrefixes { get; set; } string? ContactName { get; set; } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/IRegenerateApiClientSecretCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/IRegenerateApiClientSecretCommand.cs new file mode 100644 index 000000000..2377be9fc --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/IRegenerateApiClientSecretCommand.cs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IRegenerateApiClientSecretCommand +{ + RegenerateApiClientSecretResult Execute(int apiClientId); +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/RegenerateApiClientSecretCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/RegenerateApiClientSecretCommand.cs index 7d0d6d7b7..a23a8f6ac 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/RegenerateApiClientSecretCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/RegenerateApiClientSecretCommand.cs @@ -1,52 +1,55 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Admin.DataAccess.Contexts; -using EdFi.Admin.DataAccess.Models; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using Microsoft.EntityFrameworkCore; - -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; - -public class RegenerateApiClientSecretCommand -{ - private readonly IUsersContext _context; - - public RegenerateApiClientSecretCommand(IUsersContext context) - { - _context = context; - } - - public RegenerateApiClientSecretResult Execute(int applicationId) - { - var application = _context.Applications - .Include(x => x.ApiClients) - .SingleOrDefault(a => a.ApplicationId == applicationId); - if (application == null) - { - throw new NotFoundException("application", applicationId); - } - - var apiClient = application.ApiClients.First(); - - apiClient.GenerateSecret(); - apiClient.SecretIsHashed = false; - _context.SaveChanges(); - - return new RegenerateApiClientSecretResult - { - Key = apiClient.Key, - Secret = apiClient.Secret, - Application = application - }; - } -} - -public class RegenerateApiClientSecretResult -{ - public string? Key { get; set; } - public string? Secret { get; set; } - public Application Application { get; set; } = new(); -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public class RegenerateApiClientSecretCommand : IRegenerateApiClientSecretCommand +{ + private readonly IUsersContext _context; + + public RegenerateApiClientSecretCommand(IUsersContext context) + { + _context = context; + } + + public RegenerateApiClientSecretResult Execute(int apiClientId) + { + var apiClient = _context.ApiClients + .Include(a => a.Application) + .SingleOrDefault(a => a.ApiClientId == apiClientId); + if (apiClient == null) + { + throw new NotFoundException("ApiClient", apiClientId); + } + + apiClient.GenerateSecret(); + apiClient.SecretIsHashed = false; + _context.SaveChanges(); + + return new RegenerateApiClientSecretResult + { + Id = apiClient.ApiClientId, + Name = apiClient.Name, + Key = apiClient.Key, + Secret = apiClient.Secret, + Application = apiClient.Application + }; + } +} + +public class RegenerateApiClientSecretResult +{ + public int Id { get; set; } + public string? Name { get; set; } + public string? Key { get; set; } + public string? Secret { get; set; } + public Application Application { get; set; } = new(); +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/RegenerateApplicationApiClientSecretCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/RegenerateApplicationApiClientSecretCommand.cs new file mode 100644 index 000000000..23667654b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/RegenerateApplicationApiClientSecretCommand.cs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public class RegenerateApplicationApiClientSecretCommand +{ + private readonly IUsersContext _context; + + public RegenerateApplicationApiClientSecretCommand(IUsersContext context) + { + _context = context; + } + + public RegenerateApplicationApiClientSecretResult Execute(int applicationId) + { + var application = _context.Applications + .Include(x => x.ApiClients) + .SingleOrDefault(a => a.ApplicationId == applicationId); + if (application == null) + { + throw new NotFoundException("application", applicationId); + } + + var apiClient = application.ApiClients.First(); + + apiClient.GenerateSecret(); + apiClient.SecretIsHashed = false; + _context.SaveChanges(); + + return new RegenerateApplicationApiClientSecretResult + { + Key = apiClient.Key, + Secret = apiClient.Secret, + Application = application + }; + } +} + +public class RegenerateApplicationApiClientSecretResult +{ + public string? Key { get; set; } + public string? Secret { get; set; } + public Application Application { get; set; } = new(); +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/ValidationConstants.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/ValidationConstants.cs index c70417bb2..42a495e47 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/ValidationConstants.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/ValidationConstants.cs @@ -7,5 +7,6 @@ namespace EdFi.Ods.AdminApi.Infrastructure.Commands; public static class ValidationConstants { - public static int MaximumApplicationNameLength = 50; + public const int MaximumApplicationNameLength = 50; + public const int MaximumApiClientNameLength = 50; } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetAllActionsQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetAllActionsQuery.cs new file mode 100644 index 000000000..c75de7cbc --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetAllActionsQuery.cs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq.Expressions; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using EdFi.Security.DataAccess.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Action = EdFi.Security.DataAccess.Models.Action; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetAllActionsQuery +{ + IReadOnlyList Execute(); + IReadOnlyList Execute(CommonQueryParams commonQueryParams, int? id, string? name); +} + +public class GetAllActionsQuery : IGetAllActionsQuery +{ + private readonly ISecurityContext _securityContext; + private readonly IOptions _options; + private readonly Dictionary>> _orderByColumnActions; + + public GetAllActionsQuery(ISecurityContext securityContext, IOptions options) + { + _securityContext = securityContext; + _options = options; + var isSQLServerEngine = _options.Value.DatabaseEngine?.ToLowerInvariant() == DatabaseEngineEnum.SqlServer.ToLowerInvariant(); + _orderByColumnActions = new Dictionary>> + (StringComparer.OrdinalIgnoreCase) + { + { SortingColumns.DefaultNameColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.ActionName, DatabaseEngineEnum.SqlServerCollation) : x.ActionName }, + { SortingColumns.ActionUriColumn, x => x.ActionUri }, + { SortingColumns.DefaultIdColumn, x => x.ActionId } + }; + } + + public IReadOnlyList Execute() + { + return _securityContext.Actions.ToList(); + } + + public IReadOnlyList Execute(CommonQueryParams commonQueryParams, int? id, string? name) + { + Expression> columnToOrderBy = _orderByColumnActions.GetColumnToOrderBy(commonQueryParams.OrderBy); + + return _securityContext.Actions + .Where(a => id == null || a.ActionId == id) + .Where(a => name == null || a.ActionName == name) + .OrderByColumn(columnToOrderBy, commonQueryParams.IsDescending) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .ToList(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetAllApplicationsQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetAllApplicationsQuery.cs new file mode 100644 index 000000000..c841f32db --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetAllApplicationsQuery.cs @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq.Expressions; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetAllApplicationsQuery +{ + IReadOnlyList Execute(CommonQueryParams commonQueryParams, int? id, string? applicationName, string? claimsetName, string? ids); +} + +public class GetAllApplicationsQuery : IGetAllApplicationsQuery +{ + private readonly IUsersContext _context; + private readonly IOptions _options; + private readonly Dictionary>> _orderByColumnApplications; + + public GetAllApplicationsQuery(IUsersContext context, IOptions options) + { + _context = context; + _options = options; + var isSQLServerEngine = _options.Value.DatabaseEngine?.ToLowerInvariant() == DatabaseEngineEnum.SqlServer.ToLowerInvariant(); + _orderByColumnApplications = new Dictionary>> + (StringComparer.OrdinalIgnoreCase) + { + { SortingColumns.ApplicationNameColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.ApplicationName, DatabaseEngineEnum.SqlServerCollation) : x.ApplicationName }, + { SortingColumns.ApplicationClaimSetNameColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.ClaimSetName, DatabaseEngineEnum.SqlServerCollation) : x.ClaimSetName }, + { SortingColumns.DefaultIdColumn, x => x.ApplicationId } + }; + } + + public IReadOnlyList Execute(CommonQueryParams commonQueryParams, int? id, string? applicationName, string? claimsetName, string? ids) + { + Expression> columnToOrderBy = _orderByColumnApplications.GetColumnToOrderBy(commonQueryParams.OrderBy); + + var applications = _context.Applications + .Include(a => a.ApplicationEducationOrganizations) + .Include(a => a.Profiles) + .Include(a => a.Vendor) + .Include(a => a.ApiClients) + .Where(a => id == null || a.ApplicationId == id) + .Where(a => applicationName == null || a.ApplicationName == applicationName) + .Where(a => claimsetName == null || a.ClaimSetName == claimsetName) + .Where(a => ids == null || ids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).Contains(a.ApplicationId)) + .OrderByColumn(columnToOrderBy, commonQueryParams.IsDescending) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .ToList(); + + return applications; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetAllClaimSetsQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetAllClaimSetsQuery.cs new file mode 100644 index 000000000..41ecac824 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetAllClaimSetsQuery.cs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq.Expressions; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using EdFi.Security.DataAccess.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using ClaimSet = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ClaimSet; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetAllClaimSetsQuery +{ + IReadOnlyList Execute(); + IReadOnlyList Execute(CommonQueryParams commonQueryParams, int? id, string? name); +} + +public class GetAllClaimSetsQuery : IGetAllClaimSetsQuery +{ + private readonly ISecurityContext _securityContext; + private readonly IOptions _options; + private readonly Dictionary>> _orderByColumnClaimSet; + public GetAllClaimSetsQuery(ISecurityContext securityContext, IOptions options) + { + _securityContext = securityContext; + _options = options; + var isSQLServerEngine = _options.Value.DatabaseEngine?.ToLowerInvariant() == DatabaseEngineEnum.SqlServer.ToLowerInvariant(); + _orderByColumnClaimSet = new Dictionary>> + (StringComparer.OrdinalIgnoreCase) + { + #pragma warning disable CS8603 // Possible null reference return. + { SortingColumns.DefaultNameColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.Name, DatabaseEngineEnum.SqlServerCollation) : x.Name }, + #pragma warning restore CS8603 // Possible null reference return. + { SortingColumns.DefaultIdColumn, x => x.Id } + }; + } + + public IReadOnlyList Execute() + { + return _securityContext.ClaimSets + .Select(x => new ClaimSet + { + Id = x.ClaimSetId, + Name = x.ClaimSetName, + IsEditable = !x.IsEdfiPreset && !x.ForApplicationUseOnly + }) + .Distinct() + .OrderBy(x => x.Name) + .ToList(); + } + + public IReadOnlyList Execute(CommonQueryParams commonQueryParams, int? id, string? name) + { + Expression> columnToOrderBy = _orderByColumnClaimSet.GetColumnToOrderBy(commonQueryParams.OrderBy); + + return _securityContext.ClaimSets + .Where(c => id == null || c.ClaimSetId == id) + .Where(c => name == null || c.ClaimSetName == name) + .Select(x => new ClaimSet + { + Id = x.ClaimSetId, + Name = x.ClaimSetName, + IsEditable = !x.IsEdfiPreset && !x.ForApplicationUseOnly + }) + .Distinct() + .OrderByColumn(columnToOrderBy, commonQueryParams.IsDescending) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .ToList(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApiClientByIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApiClientByIdQuery.cs new file mode 100644 index 000000000..1838c0cd8 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApiClientByIdQuery.cs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public class GetApiClientByIdQuery : IGetApiClientByIdQuery +{ + private readonly IUsersContext _context; + + public GetApiClientByIdQuery(IUsersContext context) + { + _context = context; + } + + public ApiClient Execute(int apiClientId) + { + var apiClient = _context.ApiClients + .Include(a => a.Application) + .SingleOrDefault(app => app.ApiClientId == apiClientId); + + if (apiClient == null) + { + throw new NotFoundException("apiclient", apiClientId); + } + + return apiClient; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApiClientIdByApplicationIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApiClientIdByApplicationIdQuery.cs new file mode 100644 index 000000000..3a879aa63 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApiClientIdByApplicationIdQuery.cs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetApiClientIdByApplicationIdQuery +{ + ApiClient Execute(int applicationId); +} + +public class GetApiClientIdByApplicationIdQuery : IGetApiClientIdByApplicationIdQuery +{ + private readonly IUsersContext _context; + + public GetApiClientIdByApplicationIdQuery(IUsersContext context) + { + _context = context; + } + + public ApiClient Execute(int applicationId) + { + var apiClientId = _context.ApiClients + .FirstOrDefault(app => app.Application.ApplicationId == applicationId); + if (apiClientId == null) + { + throw new NotFoundException("apiClientId", applicationId); + } + + return apiClientId; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApiClientOdsInstanceQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApiClientOdsInstanceQuery.cs new file mode 100644 index 000000000..bd0e2ed72 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApiClientOdsInstanceQuery.cs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetApiClientOdsInstanceQuery +{ + ApiClientOdsInstance? Execute(int apiClientId, int odsInstanceId); +} + +public class GetApiClientOdsInstanceQuery : IGetApiClientOdsInstanceQuery +{ + private readonly IUsersContext _usersContext; + + public GetApiClientOdsInstanceQuery(IUsersContext userContext) + { + _usersContext = userContext; + } + public ApiClientOdsInstance? Execute(int apiClientId, int odsInstanceId) + { + var result = _usersContext.ApiClientOdsInstances + .SingleOrDefault(odsInstance => odsInstance.ApiClient.ApiClientId == apiClientId && odsInstance.OdsInstance.OdsInstanceId == odsInstanceId); + return result; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApiClientsByApplicationIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApiClientsByApplicationIdQuery.cs new file mode 100644 index 000000000..e085133fc --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApiClientsByApplicationIdQuery.cs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Common.Extensions; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public class GetApiClientsByApplicationIdQuery : IGetApiClientsByApplicationIdQuery +{ + private readonly IUsersContext _context; + + public GetApiClientsByApplicationIdQuery(IUsersContext context) + { + _context = context; + } + + public IReadOnlyList Execute(int applicationId) + { + var apiClients = _context.ApiClients + .Include(ac => ac.Application) + .Include(ac => ac.User) + .Where(app => applicationId == 0 || app.Application.ApplicationId == applicationId); + + return apiClients.ToReadOnlyList(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationByIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationByIdQuery.cs index 5c9069c90..eb5eb9c57 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationByIdQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationByIdQuery.cs @@ -3,11 +3,11 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System.Linq; using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; -using Microsoft.EntityFrameworkCore; - +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; public class GetApplicationByIdQuery @@ -21,12 +21,11 @@ public GetApplicationByIdQuery(IUsersContext context) public Application Execute(int applicationId) { - var application = _context.Applications - .Include(x => x.Vendor) - .Include(x => x.ApplicationEducationOrganizations) - .Include(x => x.Profiles) - .Include(x => x.OdsInstance) - .Include(x => x.ApiClients) + var application = _context.Applications + .Include(a => a.ApplicationEducationOrganizations) + .Include(a => a.Profiles) + .Include(a => a.Vendor) + .Include(a => a.ApiClients) .SingleOrDefault(app => app.ApplicationId == applicationId); if (application == null) { diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationByNameAndClaimsetQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationByNameAndClaimsetQuery.cs new file mode 100644 index 000000000..d3202ca2b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationByNameAndClaimsetQuery.cs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetApplicationByNameAndClaimsetQuery +{ + Application? Execute(string applicationName, string claimset); +} + +public class GetApplicationByNameAndClaimsetQuery : IGetApplicationByNameAndClaimsetQuery +{ + private readonly IUsersContext _context; + + public GetApplicationByNameAndClaimsetQuery(IUsersContext context) + { + _context = context; + } + + public Application? Execute(string applicationName, string claimset) + { + var application = _context.Applications + .Include(a => a.ApplicationEducationOrganizations) + .Include(a => a.Profiles) + .Include(a => a.Vendor) + .SingleOrDefault(app => app.ApplicationName == applicationName && app.ClaimSetName == claimset); + return application; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationsByOdsInstanceIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationsByOdsInstanceIdQuery.cs index 990165624..464806bbd 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationsByOdsInstanceIdQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationsByOdsInstanceIdQuery.cs @@ -5,6 +5,7 @@ using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; using Microsoft.EntityFrameworkCore; namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; @@ -23,21 +24,23 @@ public GetApplicationsByOdsInstanceIdQuery(IUsersContext context) _context = context; } - public List Execute(int odsInstanceId) - { - var applications = _context.Applications - .Include(aco => aco.OdsInstance) - .Include(aco => aco.ApplicationEducationOrganizations) - .Include(api => api.Profiles) - .Include(api => api.Vendor) - .Where(a => a.OdsInstance.OdsInstanceId == odsInstanceId) - .ToList(); - - if (!applications.Any() && _context.OdsInstances.Find(odsInstanceId) == null) - { - throw new NotFoundException("odsinstance", odsInstanceId); - } - - return applications; + public List Execute(int odsInstanceId) + { + var applications = _context.ApiClientOdsInstances + .Where(aco => aco.OdsInstance.OdsInstanceId == odsInstanceId) + .Select(aco => aco.ApiClient.Application) + .Distinct() + .Include(app => app.ApplicationEducationOrganizations) + .Include(app => app.Profiles) + .Include(app => app.Vendor) + .Include(app => app.ApiClients) + .ToList(); + + if (!applications.Any() && _context.OdsInstances.Find(odsInstanceId) == null) + { + throw new NotFoundException("odsinstance", odsInstanceId); + } + + return applications; } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationsByVendorIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationsByVendorIdQuery.cs index 2befccf2e..6de0a3ddb 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationsByVendorIdQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetApplicationsByVendorIdQuery.cs @@ -5,8 +5,9 @@ using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; -using Microsoft.EntityFrameworkCore; - +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; public class GetApplicationsByVendorIdQuery @@ -20,11 +21,11 @@ public GetApplicationsByVendorIdQuery(IUsersContext context) public List Execute(int vendorid) { - var applications = _context.Applications - .Include(x=> x.Profiles) - .Include(x => x.OdsInstance) - .Include(x => x.ApplicationEducationOrganizations) - .Include(x => x.Vendor) + var applications = _context.Applications + .Include(a => a.ApplicationEducationOrganizations) + .Include(a => a.Profiles) + .Include(a => a.Vendor) + .Include(a => a.ApiClients) .Where(a => a.Vendor != null && a.Vendor.VendorId == vendorid) .ToList(); diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetAuthStrategiesQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetAuthStrategiesQuery.cs new file mode 100644 index 000000000..f07efa67e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetAuthStrategiesQuery.cs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq.Expressions; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using EdFi.Security.DataAccess.Contexts; +using EdFi.Security.DataAccess.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetAuthStrategiesQuery +{ + List Execute(); + List Execute(CommonQueryParams commonQueryParams); +} + +public class GetAuthStrategiesQuery : IGetAuthStrategiesQuery +{ + private readonly ISecurityContext _context; + private readonly IOptions _options; + private readonly Dictionary>> _orderByColumnAuthorizationStrategies; + public GetAuthStrategiesQuery(ISecurityContext context, IOptions options) + { + _context = context; + _options = options; + var isSQLServerEngine = _options.Value.DatabaseEngine?.ToLowerInvariant() == DatabaseEngineEnum.SqlServer.ToLowerInvariant(); + + _orderByColumnAuthorizationStrategies = new Dictionary>> + (StringComparer.OrdinalIgnoreCase) + { + { SortingColumns.DefaultNameColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.AuthorizationStrategyName, DatabaseEngineEnum.SqlServerCollation) : x.AuthorizationStrategyName }, + { SortingColumns.AuthorizationStrategyDisplayNameColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.DisplayName, DatabaseEngineEnum.SqlServerCollation) : x.DisplayName }, + { SortingColumns.DefaultIdColumn, x => x.AuthorizationStrategyId } + }; + } + + public List Execute() + { + return _context.AuthorizationStrategies.OrderBy(v => v.AuthorizationStrategyName).ToList(); + } + + public List Execute(CommonQueryParams commonQueryParams) + { + Expression> columnToOrderBy = _orderByColumnAuthorizationStrategies.GetColumnToOrderBy(commonQueryParams.OrderBy); + + return _context.AuthorizationStrategies + .OrderByColumn(columnToOrderBy, commonQueryParams.IsDescending) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .ToList(); + } +} + diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceContextByIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceContextByIdQuery.cs new file mode 100644 index 000000000..ca3339a4e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceContextByIdQuery.cs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetOdsInstanceContextByIdQuery +{ + OdsInstanceContext Execute(int odsInstanceContextId); +} + +public class GetOdsInstanceContextByIdQuery : IGetOdsInstanceContextByIdQuery +{ + private readonly IUsersContext _context; + + public GetOdsInstanceContextByIdQuery(IUsersContext context) + { + _context = context; + } + + public OdsInstanceContext Execute(int odsInstanceContextId) + { + var odsInstanceContext = _context.OdsInstanceContexts + .Include(oid => oid.OdsInstance) + .SingleOrDefault(app => app.OdsInstanceContextId == odsInstanceContextId); + if (odsInstanceContext == null) + { + throw new NotFoundException("odsInstanceContext", odsInstanceContextId); + } + + return odsInstanceContext; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceContextsQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceContextsQuery.cs new file mode 100644 index 000000000..305480cd1 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceContextsQuery.cs @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq.Expressions; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + + +public interface IGetOdsInstanceContextsQuery +{ + List Execute(); + List Execute(CommonQueryParams commonQueryParams); +} + +public class GetOdsInstanceContextsQuery : IGetOdsInstanceContextsQuery +{ + private readonly IUsersContext _usersContext; + private readonly IOptions _options; + private readonly Dictionary>> _orderByColumnOds; + public GetOdsInstanceContextsQuery(IUsersContext usersContext, IOptions options) + { + _usersContext = usersContext; + _options = options; + var isSQLServerEngine = _options.Value.DatabaseEngine?.ToLowerInvariant() == DatabaseEngineEnum.SqlServer.ToLowerInvariant(); + _orderByColumnOds = new Dictionary>> + (StringComparer.OrdinalIgnoreCase) + { + { SortingColumns.OdsInstanceContextKeyColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.ContextKey, DatabaseEngineEnum.SqlServerCollation) : x.ContextKey }, + { SortingColumns.OdsInstanceContextValueColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.ContextValue, DatabaseEngineEnum.SqlServerCollation) : x.ContextValue }, + { SortingColumns.DefaultIdColumn, x => x.OdsInstanceContextId } + }; + } + + public List Execute() + { + return _usersContext.OdsInstanceContexts + .Include(oid => oid.OdsInstance) + .OrderBy(p => p.ContextKey).ToList(); + } + + public List Execute(CommonQueryParams commonQueryParams) + { + Expression> columnToOrderBy = _orderByColumnOds.GetColumnToOrderBy(commonQueryParams.OrderBy); + + return _usersContext.OdsInstanceContexts + .Include(oid => oid.OdsInstance) + .OrderByColumn(columnToOrderBy, commonQueryParams.IsDescending) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .ToList(); + } +} + diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceDerivativeByIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceDerivativeByIdQuery.cs new file mode 100644 index 000000000..edceffaa9 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceDerivativeByIdQuery.cs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetOdsInstanceDerivativeByIdQuery +{ + OdsInstanceDerivative Execute(int odsInstanceDerivativeId); +} + +public class GetOdsInstanceDerivativeByIdQuery : IGetOdsInstanceDerivativeByIdQuery +{ + private readonly IUsersContext _context; + + public GetOdsInstanceDerivativeByIdQuery(IUsersContext context) + { + _context = context; + } + + public OdsInstanceDerivative Execute(int odsInstanceDerivativeId) + { + var odsInstanceDerivative = _context.OdsInstanceDerivatives + .Include(oid => oid.OdsInstance) + .SingleOrDefault(app => app.OdsInstanceDerivativeId == odsInstanceDerivativeId); + if (odsInstanceDerivative == null) + { + throw new NotFoundException("odsInstanceDerivative", odsInstanceDerivativeId); + } + + return odsInstanceDerivative; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceDerivativesQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceDerivativesQuery.cs new file mode 100644 index 000000000..4bf1e7964 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceDerivativesQuery.cs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq.Expressions; +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetOdsInstanceDerivativesQuery +{ + List Execute(); + List Execute(CommonQueryParams commonQueryParams); +} + +public class GetOdsInstanceDerivativesQuery : IGetOdsInstanceDerivativesQuery +{ + private readonly IUsersContext _usersContext; + private readonly IOptions _options; + private readonly Dictionary>> _orderByColumnOds; + public GetOdsInstanceDerivativesQuery(IUsersContext usersContext, IOptions options) + { + _usersContext = usersContext; + _options = options; + var DatabaseEngine = _options.Value.DatabaseEngine ??= DatabaseEngineEnum.SqlServer; + var isSQLServerEngine = DatabaseEngine.Equals(DatabaseEngineEnum.SqlServer, StringComparison.OrdinalIgnoreCase); + _orderByColumnOds = new Dictionary>> + (StringComparer.OrdinalIgnoreCase) + { + { SortingColumns.OdsInstanceDerivativeTypeColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.DerivativeType, DatabaseEngineEnum.SqlServerCollation) : x.DerivativeType }, + { SortingColumns.OdsInstanceDerivativeOdsInstanceIdColumn, x => x.OdsInstance.OdsInstanceId }, + { SortingColumns.DefaultIdColumn, x => x.OdsInstanceDerivativeId } + }; + } + + public List Execute() + { + return [.. _usersContext.OdsInstanceDerivatives + .Include(oid => oid.OdsInstance) + .OrderBy(p => p.DerivativeType)]; + } + + public List Execute(CommonQueryParams commonQueryParams) + { + Expression> columnToOrderBy = _orderByColumnOds.GetColumnToOrderBy(commonQueryParams.OrderBy); + + return [.. _usersContext.OdsInstanceDerivatives + .Include(oid => oid.OdsInstance) + .OrderByColumn(columnToOrderBy, commonQueryParams.IsDescending) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options)]; + } +} + diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceQuery.cs index ddd01dc9c..83b934aef 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceQuery.cs @@ -5,7 +5,9 @@ using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; - +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using Microsoft.EntityFrameworkCore; + namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; public interface IGetOdsInstanceQuery @@ -13,18 +15,15 @@ public interface IGetOdsInstanceQuery OdsInstance Execute(int odsInstanceId); } -public class GetOdsInstanceQuery : IGetOdsInstanceQuery +public class GetOdsInstanceQuery(IUsersContext userContext) : IGetOdsInstanceQuery { - private readonly IUsersContext _usersContext; - - public GetOdsInstanceQuery(IUsersContext userContext) - { - _usersContext = userContext; - } + private readonly IUsersContext _usersContext = userContext; public OdsInstance Execute(int odsInstanceId) { - return _usersContext.OdsInstances + return _usersContext.OdsInstances + .Include(p => p.OdsInstanceContexts) + .Include(p => p.OdsInstanceDerivatives) .SingleOrDefault(odsInstance => odsInstance.OdsInstanceId == odsInstanceId) ?? throw new NotFoundException("odsInstance", odsInstanceId); } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstancesQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstancesQuery.cs index 59631f6f2..6e997d740 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstancesQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstancesQuery.cs @@ -3,10 +3,15 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using System.Linq.Expressions; using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; -using EdFi.Ods.AdminApi.Helpers; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; @@ -15,18 +20,27 @@ public interface IGetOdsInstancesQuery { List Execute(); - List Execute(CommonQueryParams commonQueryParams); + List Execute(CommonQueryParams commonQueryParams, int? id, string? name, string? instanceType); } public class GetOdsInstancesQuery : IGetOdsInstancesQuery { private readonly IUsersContext _usersContext; - private readonly IOptions _options; + private readonly IOptions _options; + private readonly Dictionary>> _orderByColumnOds; public GetOdsInstancesQuery(IUsersContext userContext, IOptions options) { - _usersContext = userContext; - _options = options; + _usersContext = userContext; + _options = options; + var isSQLServerEngine = _options.Value.DatabaseEngine?.ToLowerInvariant() == DatabaseEngineEnum.SqlServer.ToLowerInvariant(); + _orderByColumnOds = new Dictionary>> + (StringComparer.OrdinalIgnoreCase) + { + { SortingColumns.DefaultNameColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.Name, DatabaseEngineEnum.SqlServerCollation) : x.Name }, + { SortingColumns.OdsInstanceInstanceTypeColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.InstanceType, DatabaseEngineEnum.SqlServerCollation) : x.InstanceType }, + { SortingColumns.DefaultIdColumn, x => x.OdsInstanceId } + }; } public List Execute() @@ -34,11 +48,16 @@ public List Execute() return _usersContext.OdsInstances.OrderBy(odsInstance => odsInstance.Name).ToList(); } - public List Execute(CommonQueryParams commonQueryParams) - { + public List Execute(CommonQueryParams commonQueryParams, int? id, string? name, string? instanceType) + { + Expression> columnToOrderBy = _orderByColumnOds.GetColumnToOrderBy(commonQueryParams.OrderBy); + return _usersContext.OdsInstances - .OrderBy(o => o.Name) - .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .Where(o => id == null || o.OdsInstanceId == id) + .Where(o => name == null || o.Name == name) + .Where(o => instanceType == null || o.InstanceType == instanceType) + .OrderByColumn(columnToOrderBy, commonQueryParams.IsDescending) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) .ToList(); } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetProfileByIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetProfileByIdQuery.cs new file mode 100644 index 000000000..f9172fa57 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetProfileByIdQuery.cs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetProfileByIdQuery +{ + Profile Execute(int profileId); +} + +public class GetProfileByIdQuery : IGetProfileByIdQuery +{ + private readonly IUsersContext _context; + + public GetProfileByIdQuery(IUsersContext context) + { + _context = context; + } + + public Profile Execute(int profileId) + { + var profile = _context.Profiles.SingleOrDefault(app => app.ProfileId == profileId); + if (profile == null) + { + throw new NotFoundException("profile", profileId); + } + + return profile; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetProfilesQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetProfilesQuery.cs index 22cb28b30..56cde4160 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetProfilesQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetProfilesQuery.cs @@ -3,24 +3,56 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System.Collections.Generic; -using System.Linq; +using System.Linq.Expressions; using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -public class GetProfilesQuery +public interface IGetProfilesQuery { - private readonly IUsersContext _usersContext; + List Execute(); + List Execute(CommonQueryParams commonQueryParams, int? id, string? name); +} - public GetProfilesQuery(IUsersContext usersContext) +public class GetProfilesQuery : IGetProfilesQuery +{ + private readonly IUsersContext _usersContext; + private readonly IOptions _options; + private readonly Dictionary>> _orderByColumnProfiles; + public GetProfilesQuery(IUsersContext usersContext, IOptions options) { - _usersContext = usersContext; + _usersContext = usersContext; + _options = options; + var isSQLServerEngine = _options.Value.DatabaseEngine?.ToLowerInvariant() == DatabaseEngineEnum.SqlServer.ToLowerInvariant(); + _orderByColumnProfiles = new Dictionary>> + (StringComparer.OrdinalIgnoreCase) + { + { SortingColumns.DefaultNameColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.ProfileName, DatabaseEngineEnum.SqlServerCollation) : x.ProfileName }, + { SortingColumns.DefaultIdColumn, x => x.ProfileId } + }; } public List Execute() { return _usersContext.Profiles.OrderBy(p => p.ProfileName).ToList(); } + public List Execute(CommonQueryParams commonQueryParams, int? id, string? name) + { + Expression> columnToOrderBy = _orderByColumnProfiles.GetColumnToOrderBy(commonQueryParams.OrderBy); + + return _usersContext.Profiles + .Where(p => id == null || p.ProfileId == id) + .Where(p => name == null || p.ProfileName == name) + .OrderByColumn(columnToOrderBy, commonQueryParams.IsDescending) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .ToList(); + } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimActionAuthorizationStrategiesQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimActionAuthorizationStrategiesQuery.cs new file mode 100644 index 000000000..6a545ede2 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimActionAuthorizationStrategiesQuery.cs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq.Expressions; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Features.ResourceClaimActionAuthStrategies; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using EdFi.Security.DataAccess.Contexts; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetResourceClaimActionAuthorizationStrategiesQuery +{ + public IReadOnlyList Execute(CommonQueryParams commonQueryParams, string? resourceName); +} + +public class GetResourceClaimActionAuthorizationStrategiesQuery : IGetResourceClaimActionAuthorizationStrategiesQuery +{ + private readonly ISecurityContext _securityContext; + private readonly IOptions _options; + private readonly Dictionary>> _orderByColumns; + + public GetResourceClaimActionAuthorizationStrategiesQuery(ISecurityContext securityContext, IOptions options) + { + _securityContext = securityContext; + _options = options; + _orderByColumns = new Dictionary>> + (StringComparer.OrdinalIgnoreCase) + { + { SortingColumns.DefaultIdColumn, x => x.ResourceClaimId }, + { nameof(ResourceClaimActionAuthStrategyModel.ResourceName), x => x.ResourceName }, + { nameof(ResourceClaimActionAuthStrategyModel.ClaimName), x => x.ClaimName } + }; + } + + public IReadOnlyList Execute(CommonQueryParams commonQueryParams, string? resourceName) + { + Expression> columnToOrderBy = _orderByColumns.GetColumnToOrderBy(commonQueryParams.OrderBy); + + return _securityContext.ResourceClaimActionAuthorizationStrategies + // Group by ResourceClaimId and ResourceName to structure the JSON correctly + .Where(w => resourceName == null || w.ResourceClaimAction.ResourceClaim.ResourceName == resourceName) + .GroupBy(gb => new + { + gb.ResourceClaimAction.ResourceClaimId, + gb.ResourceClaimAction.ResourceClaim.ResourceName, + gb.ResourceClaimAction.ResourceClaim.ClaimName + }) + .Select(group => new ResourceClaimActionAuthStrategyModel + { + ResourceClaimId = group.Key.ResourceClaimId, + ResourceName = group.Key.ResourceName, + ClaimName = group.Key.ClaimName, + // Group by ActionId and ActionName to create a list of actions within the resource + AuthorizationStrategiesForActions = group.GroupBy(gb => new + { + gb.ResourceClaimAction.Action.ActionId, + gb.ResourceClaimAction.Action.ActionName + }) + .Select(groupedActions => new ActionWithAuthorizationStrategy + { + ActionId = groupedActions.Key.ActionId, + ActionName = groupedActions.Key.ActionName, + // For each action, get the associated authorization strategies + AuthorizationStrategies = groupedActions.Select(resourceClaimActionAuthorizationStrategies => + new AuthorizationStrategyModelForAction + { + AuthStrategyId = resourceClaimActionAuthorizationStrategies.AuthorizationStrategy.AuthorizationStrategyId, + AuthStrategyName = resourceClaimActionAuthorizationStrategies.AuthorizationStrategy.AuthorizationStrategyName, + }).ToList() + }).ToList() + }) + .OrderByColumn(columnToOrderBy, commonQueryParams.IsDescending) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .ToList(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimActionsQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimActionsQuery.cs new file mode 100644 index 000000000..addee807e --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimActionsQuery.cs @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq.Expressions; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Features.ResourceClaimActions; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using EdFi.Security.DataAccess.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetResourceClaimActionsQuery +{ + public IReadOnlyList Execute(CommonQueryParams commonQueryParams, string? resourceName); +} + +public class GetResourceClaimActionsQuery : IGetResourceClaimActionsQuery +{ + private readonly ISecurityContext _securityContext; + private readonly IOptions _options; + private readonly Dictionary>> _orderByColumns; + + public GetResourceClaimActionsQuery(ISecurityContext securityContext, IOptions options) + { + _securityContext = securityContext; + _options = options; + var isSQLServerEngine = _options.Value.DatabaseEngine?.ToLowerInvariant() == DatabaseEngineEnum.SqlServer.ToLowerInvariant(); + _orderByColumns = new Dictionary>> + (StringComparer.OrdinalIgnoreCase) + { + { nameof(ResourceClaimActionModel.ResourceClaimId), x => x.ResourceClaimId}, + { nameof(ResourceClaimActionModel.ResourceName), x => isSQLServerEngine ? EF.Functions.Collate(x.ResourceName, DatabaseEngineEnum.SqlServerCollation) : x.ResourceName}, + }; + } + + public IReadOnlyList Execute(CommonQueryParams commonQueryParams, string? resourceName) + { + Expression> columnToOrderBy = _orderByColumns.GetColumnToOrderBy(commonQueryParams.OrderBy); + + return _securityContext.ResourceClaimActions + .Include(i => i.ResourceClaim) + .Include(i => i.Action) + .Where(w => resourceName == null || w.ResourceClaim.ResourceName == resourceName) + .GroupBy(r => new { r.ResourceClaim.ResourceClaimId, r.ResourceClaim.ResourceName, r.ResourceClaim.ClaimName }) + .Select(group => new ResourceClaimActionModel + { + ResourceClaimId = group.Key.ResourceClaimId, + ResourceName = group.Key.ResourceName, + ClaimName = group.Key.ClaimName, + Actions = group.Select(g => new ActionForResourceClaimModel { Name = g.Action.ActionName }).Distinct().ToList() + }) + .OrderByColumn(columnToOrderBy, commonQueryParams.IsDescending) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .ToList(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimByResourceClaimIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimByResourceClaimIdQuery.cs new file mode 100644 index 000000000..afafe373f --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimByResourceClaimIdQuery.cs @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Security.DataAccess.Contexts; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetResourceClaimByResourceClaimIdQuery +{ + ResourceClaim Execute(int id); +} + +public class GetResourceClaimByResourceClaimIdQuery : IGetResourceClaimByResourceClaimIdQuery +{ + private readonly ISecurityContext _securityContext; + + public GetResourceClaimByResourceClaimIdQuery(ISecurityContext securityContext) + { + _securityContext = securityContext; + } + + public ResourceClaim Execute(int id) + { + var resource = _securityContext.ResourceClaims.FirstOrDefault(x => x.ResourceClaimId == id); + if (resource == null) + { + throw new NotFoundException("resourceclaim", id); + } + + var children = _securityContext.ResourceClaims.Where(x => x.ParentResourceClaimId == resource.ResourceClaimId); + return new ResourceClaim + { + Children = children.Select(child => new ResourceClaim() + { + Id = child.ResourceClaimId, + Name = child.ResourceName, + ParentId = resource.ResourceClaimId, + ParentName = resource.ResourceName, + }).ToList(), + Name = resource.ResourceName, + Id = resource.ResourceClaimId + }; + } +} + diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimsQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimsQuery.cs index 640ea3001..6c6bd4b74 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimsQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetResourceClaimsQuery.cs @@ -3,51 +3,93 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System.Collections.Generic; -using System.Linq; +using System.Linq.Expressions; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; using EdFi.Security.DataAccess.Contexts; +using Microsoft.Extensions.Options; namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; public interface IGetResourceClaimsQuery { IEnumerable Execute(); + IEnumerable Execute(CommonQueryParams commonQueryParams, int? id, string? name); } public class GetResourceClaimsQuery : IGetResourceClaimsQuery { private readonly ISecurityContext _securityContext; - - public GetResourceClaimsQuery(ISecurityContext securityContext) + private readonly IOptions _options; + private static readonly Dictionary>> _orderByColumnResourceClaims = + new Dictionary>> + (StringComparer.OrdinalIgnoreCase) + { + { SortingColumns.DefaultNameColumn, x => x.ResourceName }, + { SortingColumns.ResourceClaimParentNameColumn, x => x.ParentResourceClaim.ResourceName }, +#pragma warning disable CS8603 // Possible null reference return. + { SortingColumns.ResourceClaimParentIdColumn, x => x.ParentResourceClaimId }, +#pragma warning restore CS8603 // Possible null reference return. + { SortingColumns.DefaultIdColumn, x => x.ResourceClaimId } + }; + public GetResourceClaimsQuery(ISecurityContext securityContext, IOptions options) { - _securityContext = securityContext; + _securityContext = securityContext; + _options = options; } public IEnumerable Execute() { - var resources = new List(); - var parentResources = _securityContext.ResourceClaims.Where(x => x.ParentResourceClaim == null).ToList(); - var childResources = _securityContext.ResourceClaims.Where(x => x.ParentResourceClaim != null).ToList(); - foreach (var parentResource in parentResources) - { - var children = childResources.Where(x => x.ParentResourceClaimId == parentResource.ResourceClaimId); - resources.Add(new ResourceClaim - { - Children = children.Select(child => new ResourceClaim() - { - Id = child.ResourceClaimId, - Name = child.ResourceName, - ParentId = parentResource.ResourceClaimId, - ParentName = parentResource.ResourceName, - }).ToList(), - Name = parentResource.ResourceName, - Id = parentResource.ResourceClaimId - }); - } - return resources - .Distinct() - .OrderBy(x => x.Name) + return Query().ToList(); + } + + public IEnumerable Execute(CommonQueryParams commonQueryParams, int? id, string? name) + { + return Query(commonQueryParams) + .Where(c => id == null || c.Id == id) + .Where(c => name == null || c.Name == name) .ToList(); } + + private IEnumerable Query(CommonQueryParams? commonQueryParams = null) + { + Expression> columnToOrderBy = + _orderByColumnResourceClaims.GetColumnToOrderBy(commonQueryParams != null ? commonQueryParams.Value.OrderBy : string.Empty); + + var resources = new List(); + + var parentResources = _securityContext.ResourceClaims + .Where(x => x.ParentResourceClaim == null) + .OrderByColumn(columnToOrderBy, commonQueryParams.GetValueOrDefault().IsDescending); + + if (commonQueryParams != null) + parentResources = parentResources.Paginate(commonQueryParams.Value.Offset, commonQueryParams.Value.Limit, _options); + + var allResources = _securityContext.ResourceClaims.ToList(); + + foreach (var parentResource in parentResources.ToList()) + { + resources.Add(BuildResourceClaimTree(parentResource, allResources)); + } + + return resources.Distinct(); + } + + private ResourceClaim BuildResourceClaimTree(EdFi.Security.DataAccess.Models.ResourceClaim resource, List allResources) + { + var children = allResources.Where(x => x.ParentResourceClaimId == resource.ResourceClaimId).ToList(); + + return new ResourceClaim + { + Id = resource.ResourceClaimId, + Name = resource.ResourceName, + ParentId = resource.ParentResourceClaimId ?? 0, + ParentName = resource.ParentResourceClaim?.ResourceName, + Children = children.Select(child => BuildResourceClaimTree(child, allResources)).ToList() + }; + } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetVendorByIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetVendorByIdQuery.cs index ec5795040..a84f72f9d 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetVendorByIdQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetVendorByIdQuery.cs @@ -5,8 +5,8 @@ using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; -using Microsoft.EntityFrameworkCore; - +using Microsoft.EntityFrameworkCore; + namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; public interface IGetVendorByIdQuery @@ -25,9 +25,10 @@ public GetVendorByIdQuery(IUsersContext context) public Vendor? Execute(int vendorId) { - return _context.Vendors - .Include(x => x.VendorNamespacePrefixes) - .Include(x => x.Users) + return _context.Vendors + .Include(v => v.Users) + .Include(v => v.VendorNamespacePrefixes) + .Include(v => v.Applications) .SingleOrDefault(v => v.VendorId == vendorId); } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetVendorsQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetVendorsQuery.cs index 90acfec09..1ff31a2b5 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetVendorsQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetVendorsQuery.cs @@ -3,54 +3,84 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using System.Linq.Expressions; using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; -using EdFi.Ods.AdminApi.Helpers; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - +using Microsoft.Extensions.Options; + namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; public interface IGetVendorsQuery { List Execute(); - List Execute(CommonQueryParams commonQueryParams); + List Execute(CommonQueryParams commonQueryParams, int? id, string? company, string? namespacePrefixes, string? contactName, string? contactEmailAddress); } public class GetVendorsQuery : IGetVendorsQuery { private readonly IUsersContext _context; private readonly IOptions _options; - + private readonly Dictionary>> _orderByColumnVendors; public GetVendorsQuery(IUsersContext context, IOptions options) { - _context = context; - _options = options; + _context = context; + _options = options; + var isSQLServerEngine = _options.Value.DatabaseEngine?.ToLowerInvariant() == DatabaseEngineEnum.SqlServer.ToLowerInvariant(); + _orderByColumnVendors = new Dictionary>> + (StringComparer.OrdinalIgnoreCase) + { + { SortingColumns.VendorCompanyColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.VendorName, DatabaseEngineEnum.SqlServerCollation) : x.VendorName }, + #pragma warning disable CS8602 // Dereference of a possibly null reference. + { SortingColumns.VendorContactNameColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.Users.FirstOrDefault().FullName, DatabaseEngineEnum.SqlServerCollation) : x.Users.FirstOrDefault().FullName }, + { SortingColumns.VendorContactEmailColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.Users.FirstOrDefault().Email, DatabaseEngineEnum.SqlServerCollation) : x.Users.FirstOrDefault().Email }, + #pragma warning restore CS8602 // Dereference of a possibly null reference. + { SortingColumns.VendorNamespacePrefixesColumn, x => isSQLServerEngine ? EF.Functions.Collate(x.VendorNamespacePrefixes.OrderBy(p => p.NamespacePrefix).First().NamespacePrefix, DatabaseEngineEnum.SqlServerCollation) : x.VendorNamespacePrefixes.OrderBy(p => p.NamespacePrefix).First().NamespacePrefix }, + { SortingColumns.DefaultIdColumn, x => x.VendorId } + }; } public List Execute() { - return _context.Vendors - .Include(vn => vn.VendorNamespacePrefixes) - .Include(x => x.Users) - .Include(x => x.Applications).ThenInclude(x => x.ApplicationEducationOrganizations) - .Include(x => x.Applications).ThenInclude(x => x.Profiles) - .Include(x => x.Applications).ThenInclude(x => x.OdsInstance) - .OrderBy(v => v.VendorName).Where(v => !VendorExtensions.ReservedNames.Contains(v.VendorName.Trim())) - .ToList(); - } + return _context.Vendors + .Include(v => v.Applications) + .ThenInclude(a => a.Profiles) + .Include(v => v.Applications) + .ThenInclude(a => a.ApplicationEducationOrganizations) + .Include(v => v.Applications) + .ThenInclude(a => a.ApiClients) + .Include(v => v.Users) + .Include(v => v.VendorNamespacePrefixes) + .OrderBy(v => v.VendorName).Where(v => !VendorExtensions.ReservedNames.Contains(v.VendorName.Trim())).ToList(); + } - public List Execute(CommonQueryParams commonQueryParams) - { - return _context.Vendors - .Include(vn => vn.VendorNamespacePrefixes) - .Include(x => x.Users) - .Include(x => x.Applications).ThenInclude(x => x.ApplicationEducationOrganizations) - .Include(x => x.Applications).ThenInclude(x => x.Profiles) - .Include(x => x.Applications).ThenInclude(x => x.OdsInstance) - .OrderBy(v => v.VendorName).Where(v => !VendorExtensions.ReservedNames.Contains(v.VendorName.Trim())) - .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + public List Execute(CommonQueryParams commonQueryParams, int? id, string? company, string? namespacePrefixes, string? contactName, string? contactEmailAddress) + { + Expression> columnToOrderBy = _orderByColumnVendors.GetColumnToOrderBy(commonQueryParams.OrderBy); + + return _context.Vendors + .Include(v => v.Applications) + .ThenInclude(a => a.Profiles) + .Include(v => v.Applications) + .ThenInclude(a => a.ApplicationEducationOrganizations) + .Include(v => v.Applications) + .ThenInclude(a => a.ApiClients) + .Include(v => v.Users) + .Include(v => v.VendorNamespacePrefixes) + .Where(v => !VendorExtensions.ReservedNames.Contains(v.VendorName.Trim())) + .Where(c => id == null || id < 1 || c.VendorId == id) + .Where(c => company == null || c.VendorName == company) + .Where(c => namespacePrefixes == null || c.VendorNamespacePrefixes.Any(v => v.NamespacePrefix == namespacePrefixes)) + .Where(c => c.Users.Any(u => contactName == null || u.FullName == contactName)) + .Where(c => c.Users.Any(u => contactEmailAddress == null || u.Email == contactEmailAddress)) + .OrderByColumn(columnToOrderBy, commonQueryParams.IsDescending) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) .ToList(); - } + } } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/SetUpFixture.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/IGetApiClientByIdQuery.cs similarity index 60% rename from Application/EdFi.Ods.AdminApi.DBTests/SetUpFixture.cs rename to Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/IGetApiClientByIdQuery.cs index a699cbb38..b40d9c94f 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/SetUpFixture.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/IGetApiClientByIdQuery.cs @@ -3,13 +3,11 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using NUnit.Framework; +using EdFi.Admin.DataAccess.Models; -namespace EdFi.Ods.AdminApi.DBTests; +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -[SetUpFixture] -public class SetUpFixture +public interface IGetApiClientByIdQuery { - [OneTimeSetUp] - public void GlobalSetUp() => Testing.EnsureInitialized(); + ApiClient Execute(int apiClientId); } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/IGetApiClientsByApplicationIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/IGetApiClientsByApplicationIdQuery.cs new file mode 100644 index 000000000..46c94c983 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/IGetApiClientsByApplicationIdQuery.cs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Models; +using System.Collections.Generic; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetApiClientsByApplicationIdQuery +{ + IReadOnlyList Execute(int applicationId); +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/ValidateApplicationExistsQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/ValidateApplicationExistsQuery.cs new file mode 100644 index 000000000..15e89e2aa --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/ValidateApplicationExistsQuery.cs @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public class ValidateApplicationExistsQuery(IUsersContext context) +{ + private readonly IUsersContext _context = context; + + public bool Execute(IAddApplicationModel applicationModel) + { + var existingApplication = _context.Applications + .Include(a => a.ApplicationEducationOrganizations) + .Include(a => a.Profiles) + .Include(a => a.Vendor) + .Include(a => a.ApiClients) + .Where(a => a.ApplicationName == applicationModel.ApplicationName + && a.Vendor.VendorId == applicationModel.VendorId + && a.ClaimSetName == applicationModel.ClaimSetName + && (((a.ApplicationEducationOrganizations == null + || (a.ApplicationEducationOrganizations != null && !a.ApplicationEducationOrganizations.Any())) + && (applicationModel.EducationOrganizationIds == null + || (applicationModel.EducationOrganizationIds != null && !applicationModel.EducationOrganizationIds.Any()))) + || ((a.ApplicationEducationOrganizations != null && applicationModel.EducationOrganizationIds != null) + && ((!(a.ApplicationEducationOrganizations.Any() || applicationModel.EducationOrganizationIds.Any())) + || ((a.ApplicationEducationOrganizations.Any() && applicationModel.EducationOrganizationIds.Any()) + && (a.ApplicationEducationOrganizations.All(b => applicationModel.EducationOrganizationIds.Contains(b.EducationOrganizationId))) + && (a.ApplicationEducationOrganizations.Count == applicationModel.EducationOrganizationIds.Count())) + )) + )).AsEnumerable() + .Where(b => + ((b.Profiles == null + || (b.Profiles != null && b.Profiles.Count == 0)) + && (applicationModel.ProfileIds == null + || (applicationModel.ProfileIds != null && applicationModel.ProfileIds.Any()))) + || ((b.Profiles != null && applicationModel.ProfileIds != null) + && (!(b.Profiles.Count != 0 || applicationModel.ProfileIds.Any()) + || ((b.Profiles.Count != 0 && applicationModel.ProfileIds.Any()) + && (b.Profiles.Count == applicationModel.ProfileIds.Count()) + && (b.Profiles.All(c => applicationModel.ProfileIds.Contains(c.ProfileId))) + ) + ) + ) + ).Select( + applications => new + { + applications.ApplicationName, + applications.Vendor.VendorId, + ProfileIds = applications.Profiles.Select(k => k.ProfileId).ToList(), + EducationOrganizationIds = applications.ApplicationEducationOrganizations.Select(k => k.EducationOrganizationId).ToList(), + applications.ClaimSetName, + ApiClients = applications.ApiClients.Select(k => k.Application.ApplicationId).ToList(), + }).ToList(); + + var existingInstance = _context.ApiClientOdsInstances + .Include(x => x.ApiClient) + .ThenInclude(app => app.Application) + .Where(a => a.ApiClient.Application.ApplicationName == applicationModel.ApplicationName + && a.ApiClient.Application.Vendor.VendorId == applicationModel.VendorId + && a.ApiClient.Application.ClaimSetName == applicationModel.ClaimSetName) + .Select(m => new + { + m.ApiClient.Application.ApplicationId, + m.OdsInstance.OdsInstanceId + }).ToList(); + if (existingApplication.Count != 0) + { + if ((existingInstance == null + || (existingInstance != null + && existingInstance.Count == 0)) + && (applicationModel.OdsInstanceIds == null + || (applicationModel.OdsInstanceIds != null && !applicationModel.OdsInstanceIds.Any())) + ) + { + return true; + } + bool instance = existingApplication.Exists( + x => x.ApiClients.Exists(y => existingInstance != null && existingInstance.Exists(z => z.ApplicationId == y)) + ); + return instance; + } + return false; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/VendorExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/VendorExtensions.cs index b30c83b19..2957c910d 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/VendorExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/VendorExtensions.cs @@ -3,8 +3,8 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System.Linq; using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure; namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/ListExplicitSchemaDocumentFilter.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/ListExplicitSchemaDocumentFilter.cs index ea7cb7c86..bb6dce808 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/ListExplicitSchemaDocumentFilter.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/ListExplicitSchemaDocumentFilter.cs @@ -3,6 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.AdminApi.Common.Features; using EdFi.Ods.AdminApi.Features; using EdFi.Ods.AdminApi.Features.Connect; using Microsoft.OpenApi.Models; @@ -14,9 +15,7 @@ public class ListExplicitSchemaDocumentFilter : IDocumentFilter { public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { - context.SchemaGenerator.GenerateSchema(typeof(RegisterService.Request), context.SchemaRepository); - context.SchemaGenerator.GenerateSchema(typeof(AdminApiResponse), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(RegisterService.RegisterClientRequest), context.SchemaRepository); context.SchemaGenerator.GenerateSchema(typeof(AdminApiError), context.SchemaRepository); - } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/ProfileRequestExampleFilter.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/ProfileRequestExampleFilter.cs new file mode 100644 index 000000000..74c403838 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/ProfileRequestExampleFilter.cs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Newtonsoft.Json; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace EdFi.Ods.AdminApi.Infrastructure.Documentation; + +[AttributeUsage(AttributeTargets.Method)] +public class ProfileRequestExampleAttribute : Attribute +{ +} + +public class ProfileRequestExampleFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var attribute = context.MethodInfo.GetCustomAttributes(typeof(ProfileRequestExampleAttribute), false).FirstOrDefault(); + if (attribute == null) + { + return; + } + + var profileDefinition = @"" + + @"" + + @"" + + @""; + + var profileRequest = new + { + name = "Test-Profile", + definition = profileDefinition + }; + + foreach (var schema in context.SchemaRepository.Schemas) + { + if (schema.Key.ToLower().Contains("addprofilerequest") || schema.Key.ToLower().Contains("editprofilerequest")) + { + schema.Value.Example = new OpenApiString(JsonConvert.SerializeObject(profileRequest, Formatting.Indented), true); + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/SwaggerDefaultParameterFilter.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/SwaggerDefaultParameterFilter.cs index a1047221b..d269c4f96 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/SwaggerDefaultParameterFilter.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/SwaggerDefaultParameterFilter.cs @@ -3,8 +3,9 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; using EdFi.Ods.AdminApi.Features; -using EdFi.Ods.AdminApi.Helpers; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; @@ -34,11 +35,54 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) { parameter.Description = "Indicates the maximum number of items that should be returned in the results."; parameter.Schema.Default = new OpenApiString(_settings.Value.DefaultPageSizeLimit.ToString()); + } + else if (parameter.Name.ToLower().Equals("orderby")) + { + parameter.Description = "Indicates the property name by which the results will be sorted."; + parameter.Schema.Default = new OpenApiString(string.Empty); + } + else if (parameter.Name.ToLower().Equals("direction")) + { + var description = "Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC)."; + parameter.Schema.Title = description; + parameter.Description = description; + parameter.Schema.Enum = new List { new OpenApiString(SortingDirectionHelper.Direction.Ascending.ToString()), new OpenApiString(SortingDirectionHelper.Direction.Descending.ToString()) }; + parameter.Schema.Default = new OpenApiString(SortingDirectionHelper.Direction.Descending.ToString()); } } switch (context.MethodInfo.Name) { + case "GetProfiles": + { + foreach (var parameter in operation.Parameters) + { + if (parameter.Name.ToLower().Equals("id")) + { + parameter.Description = FeatureConstants.ProfileIdDescription; + } + else if (parameter.Name.ToLower().Equals("name")) + { + parameter.Description = FeatureConstants.ProfileName; + } + } + break; + } + case "GetResourceClaims": + { + foreach (var parameter in operation.Parameters) + { + if (parameter.Name.ToLower().Equals("id")) + { + parameter.Description = FeatureConstants.ResourceClaimIdDescription; + } + else if (parameter.Name.ToLower().Equals("name")) + { + parameter.Description = FeatureConstants.ResourceClaimNameDescription; + } + } + break; + } case "GetVendors": { foreach (var parameter in operation.Parameters) @@ -66,6 +110,21 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) } break; } + case "GetOdsInstances": + { + foreach (var parameter in operation.Parameters) + { + if (parameter.Name.ToLower().Equals("id")) + { + parameter.Description = FeatureConstants.OdsInstanceIdsDescription; + } + else if (parameter.Name.ToLower().Equals("name")) + { + parameter.Description = FeatureConstants.OdsInstanceName; + } + } + break; + } case "GetClaimSets": { foreach (var parameter in operation.Parameters) @@ -100,6 +159,21 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) } break; } + case "GetActions": + { + foreach (var parameter in operation.Parameters) + { + if (parameter.Name.ToLower().Equals("id")) + { + parameter.Description = FeatureConstants.ActionIdDescription; + } + else if (parameter.Name.ToLower().Equals("name")) + { + parameter.Description = FeatureConstants.ActionNameDescription; + } + } + break; + } } } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/SwaggerExcludeSchemaFilter.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/SwaggerExcludeSchemaFilter.cs index 9171e62c2..a7e063c2e 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/SwaggerExcludeSchemaFilter.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/SwaggerExcludeSchemaFilter.cs @@ -11,40 +11,23 @@ namespace EdFi.Ods.AdminApi.Infrastructure.Documentation; [AttributeUsage(AttributeTargets.Property)] public class SwaggerExcludeAttribute : Attribute -{ - public EdFiOdsSecurityModelCompatibility VersionCompatibility { get; set; } - public SwaggerExcludeAttribute(EdFiOdsSecurityModelCompatibility version = EdFiOdsSecurityModelCompatibility.Both) - { - VersionCompatibility = version; - } +{ } public class SwaggerExcludeSchemaFilter : ISchemaFilter -{ - private readonly IOdsSecurityModelVersionResolver _odsSecurityModelResolver; - - public SwaggerExcludeSchemaFilter(IOdsSecurityModelVersionResolver odsSecurityModelResolver) - { - _odsSecurityModelResolver = odsSecurityModelResolver; - } - +{ public void Apply(OpenApiSchema schema, SchemaFilterContext context) { var properties = context.Type.GetProperties(); - var version = _odsSecurityModelResolver.DetermineSecurityModel(); + foreach (var property in properties) { - var attribute = property.GetCustomAttribute(typeof(SwaggerExcludeAttribute)); + var attribute = property.GetCustomAttribute(typeof(SwaggerExcludeAttribute)); var propertyNameInCamelCasing = char.ToLowerInvariant(property.Name[0]) + property.Name[1..]; if (attribute != null) - { - var swaggerExcludeAttribute = property.CustomAttributes.First(a => a.AttributeType.Name == nameof(SwaggerExcludeAttribute)); - var odsVersionValue = swaggerExcludeAttribute.ConstructorArguments.First().Value; - if (odsVersionValue != null && - ((EdFiOdsSecurityModelCompatibility)odsVersionValue == EdFiOdsSecurityModelCompatibility.Both || - (EdFiOdsSecurityModelCompatibility)odsVersionValue == version)) - schema.Properties.Remove(propertyNameInCamelCasing); + { + schema.Properties.Remove(propertyNameInCamelCasing); } } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/SwaggerOptionalSchemaFilter.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/SwaggerOptionalSchemaFilter.cs index 5702a1719..802a6c839 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/SwaggerOptionalSchemaFilter.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/SwaggerOptionalSchemaFilter.cs @@ -5,8 +5,11 @@ using System.Reflection; using Microsoft.OpenApi.Models; +using Newtonsoft.Json; using Swashbuckle.AspNetCore.SwaggerGen; - +using System.Linq; +using EdFi.Common.Extensions; + namespace EdFi.Ods.AdminApi.Infrastructure.Documentation; [AttributeUsage(AttributeTargets.Property)] @@ -42,4 +45,17 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context) } } } -} +} + +public class SwaggerSchemaRemoveRequiredFilter : ISchemaFilter +{ + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + var properties = context.Type.GetProperties(); + foreach (var property in properties) + { + var propertyNameInCamelCasing = property.Name.ToCamelCase(); + schema.Required?.Remove(propertyNameInCamelCasing); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/TagByResourceUrlFilter.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/TagByResourceUrlFilter.cs index d39f5661b..3ced3f5be 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/TagByResourceUrlFilter.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Documentation/TagByResourceUrlFilter.cs @@ -4,6 +4,7 @@ // See the LICENSE and NOTICES files in the project root for more information. using System.Text.RegularExpressions; +using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; using EdFi.Ods.AdminApi.Infrastructure.Extensions; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; @@ -24,6 +25,6 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) ? urlParts[1] : urlParts[0]; if (!string.IsNullOrWhiteSpace(resourceName)) - operation.Tags = new List { new() { Name = resourceName.Trim('/').ToTitleCase() } }; + operation.Tags = new List { new() { Name = resourceName.Trim('/').ToPascalCase() } }; } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Enumerations.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Enumerations.cs index a9d9a018a..53a59c43b 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Enumerations.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Enumerations.cs @@ -3,9 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System; using System.Diagnostics; -using System.Linq; using System.Reflection; namespace EdFi.Ods.AdminApi.Infrastructure @@ -33,7 +31,11 @@ public static bool TryFromInt32(int listItemValue, out TEnumeration? result) [Serializable] [DebuggerDisplay("{DisplayName} - {Value}")] +#pragma warning disable S1210 // "Equals" and the comparison operators should be overridden when implementing "IComparable" +#pragma warning disable S4035 // Classes implementing "IEquatable" should be sealed public abstract class Enumeration : IComparable, IEquatable +#pragma warning restore S4035 // Classes implementing "IEquatable" should be sealed +#pragma warning restore S1210 // "Equals" and the comparison operators should be overridden when implementing "IComparable" where TEnumeration : Enumeration where TValue : IComparable { @@ -119,12 +121,6 @@ public override int GetHashCode() return Parse(displayName, "display name", item => item.DisplayName == displayName); } - public static bool TryParse(Func predicate, out TEnumeration? result) - { - result = GetAll().FirstOrDefault(predicate); - return result is not null; - } - private static TEnumeration? Parse(object value, string description, Func predicate) { if (!TryParse(predicate, out var result)) @@ -135,6 +131,11 @@ public static bool TryParse(Func predicate, out TEnumeration return result; } + public static bool TryParse(Func predicate, out TEnumeration? result) + { + result = GetAll().AsEnumerable().FirstOrDefault(predicate); + return result is not null; + } public static bool TryParse(TValue value, out TEnumeration? result) { diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/NotFoundException.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/NotFoundException.cs deleted file mode 100644 index ee5103390..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/ErrorHandling/NotFoundException.cs +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System; - -public interface INotFoundException -{ - public string Message { get; } -} - -public class NotFoundException : Exception, INotFoundException -{ - public string ResourceName { get; } - public T Id { get; } - - public NotFoundException(string resourceName, T id) - : base($"Not found: {resourceName} with ID {id}. It may have been recently deleted.") - { - ResourceName = resourceName; - Id = id; - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Extensions/AdminModelExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Extensions/AdminModelExtensions.cs index f00f8b79f..073e0fe86 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Extensions/AdminModelExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Extensions/AdminModelExtensions.cs @@ -4,7 +4,6 @@ // See the LICENSE and NOTICES files in the project root for more information. using EdFi.Admin.DataAccess.Models; -using Profile = EdFi.Ods.AdminApi.Features.Applications.Profile; namespace EdFi.Ods.AdminApi.Infrastructure; @@ -20,21 +19,12 @@ public static class AdminModelExtensions return vendor?.Users?.FirstOrDefault()?.Email; } - public static string? ProfileName(this Application application) + public static IList Profiles(this Application application) { - return application?.Profiles?.FirstOrDefault()?.ProfileName; - } - public static string? OdsInstanceName(this Application application) - { - return application?.OdsInstance?.Name; - } - - public static IList Profiles(this Application application) - { - var profiles = new List(); + var profiles = new List(); foreach (var profile in application.Profiles) { - profiles.Add(new Profile { Id = profile.ProfileId }); + profiles.Add(profile.ProfileId); } return profiles; } @@ -44,7 +34,17 @@ public static IList Profiles(this Application application) return application?.Vendor?.VendorId; } - public static IList? EducationOrganizationIds(this Application application) + public static int? OdsInstanceId(this OdsInstanceContext odsInstanceContext) + { + return odsInstanceContext.OdsInstance?.OdsInstanceId; + } + + public static int? OdsInstanceId(this OdsInstanceDerivative odsInstanceDerivative) + { + return odsInstanceDerivative.OdsInstance?.OdsInstanceId; + } + + public static IList? EducationOrganizationIds(this Application application) { return application?.ApplicationEducationOrganizations?.Select(eu => eu.EducationOrganizationId).ToList(); } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Extensions/QueryExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Extensions/QueryExtensions.cs index 16b81f87c..feec9900c 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Extensions/QueryExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Extensions/QueryExtensions.cs @@ -1,34 +1,57 @@ -using EdFi.Ods.AdminApi.Helpers; +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Linq.Expressions; +using EdFi.Ods.AdminApi.Common.Settings; using Microsoft.Extensions.Options; -namespace EdFi.Ods.AdminApi.Infrastructure.Extensions; -public static class QueryExtensions +namespace EdFi.Ods.AdminApi.Infrastructure.Extensions { - /// - /// Apply pagination based on the offset and limit - /// - /// Type of entity - /// IQueryable entity list to apply the pagination - /// - /// - /// App Setting values - /// Paginated list - public static IQueryable Paginate(this IQueryable source, int? offset, int? limit, IOptions settings) + public static class QueryExtensions { - try - { - if (offset == null) - offset = settings.Value.DefaultPageSizeOffset; - - if (limit == null) - limit = settings.Value.DefaultPageSizeLimit; - - return source.Skip(offset.Value).Take(limit.Value); - } - catch (Exception) - { - // If this throws an exception simply don't paginate. - return source; + /// + /// Ordering the IQueryable base in the expression + /// + /// Entity type + /// DBSet that contains the data + /// Expression function that contains the column to order + /// Indicate if it is descending + /// + public static IQueryable OrderByColumn(this IQueryable source, Expression> orderBy, bool isDescending) + { + if (isDescending) + source = source.OrderByDescending(orderBy); + else + source = source.OrderBy(orderBy); + return source; + } + + /// + /// Apply pagination based on the offset and limit + /// + /// Type of entity + /// IQueryable entity list to apply the pagination + /// + /// + /// App Setting values + /// Paginated list + public static IQueryable Paginate(this IQueryable source, int? offset, int? limit, IOptions settings) + { + try + { + offset ??= settings.Value.DefaultPageSizeOffset; + + limit ??= settings.Value.DefaultPageSizeLimit; + + return source.Skip(offset.Value).Take(limit.Value); + } + catch (Exception) + { + // If this throws an exception simply don't paginate. + return source; + } } } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/AdminApiFeatureHelper.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/AdminApiFeatureHelper.cs new file mode 100644 index 000000000..3601dd50f --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/AdminApiFeatureHelper.cs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using System.Reflection; + +namespace EdFi.Ods.AdminApi.Infrastructure.Helpers; + +public static class AdminApiFeatureHelper +{ + public static List GetFeatures() => FeatureHelper.GetFeatures(Assembly.GetExecutingAssembly()); +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/ConstantsHelper.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/ConstantsHelper.cs index 21c011d5c..18cf73805 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/ConstantsHelper.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/ConstantsHelper.cs @@ -12,7 +12,7 @@ public static class ConstantsHelpers /// /// Semantic version of the admin api. /// - public const string Version = "1.1"; + public const string Version = "2.0"; /// /// Assembly version of the admin api. diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/FileSystemAppSettingsFileProvider.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/FileSystemAppSettingsFileProvider.cs new file mode 100644 index 000000000..199e10cd0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/FileSystemAppSettingsFileProvider.cs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.Infrastructure.Helpers; + +public interface IAppSettingsFileProvider +{ + string ReadAllText(); + void WriteAllText(string content); +} + +public class FileSystemAppSettingsFileProvider(string filePath) : IAppSettingsFileProvider +{ + public string ReadAllText() + { + return File.ReadAllText(filePath); + } + + public void WriteAllText(string content) + { + File.WriteAllText(filePath, content); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/HealthCheckServiceExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/HealthCheckServiceExtensions.cs index 9e9696fb8..40d433983 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/HealthCheckServiceExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/HealthCheckServiceExtensions.cs @@ -3,24 +3,79 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using Microsoft.Extensions.DependencyInjection; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Common.Settings; namespace EdFi.Ods.AdminApi.Infrastructure; public static class HealthCheckServiceExtensions { - public static IServiceCollection AddHealthCheck(this IServiceCollection services, string connectionString, bool isSqlServer) + public static IServiceCollection AddHealthCheck( + this IServiceCollection services, + IConfigurationRoot configuration + ) { - var hcBuilder = services.AddHealthChecks(); - if (isSqlServer) + var databaseEngine = configuration.Get("AppSettings:DatabaseEngine", "SqlServer"); + var multiTenancyEnabled = configuration.Get("AppSettings:MultiTenancy", false); + + if (!string.IsNullOrEmpty(databaseEngine)) { - hcBuilder.AddSqlServer(connectionString); + var isSqlServer = DatabaseEngineEnum.Parse(databaseEngine).Equals(DatabaseEngineEnum.SqlServer); + var hcBuilder = services.AddHealthChecks(); + + // Add health checks for both EdFi_Admin and EdFi_Security databases + AddDatabaseHealthChecks(hcBuilder, configuration, "EdFi_Admin", multiTenancyEnabled, isSqlServer); + AddDatabaseHealthChecks(hcBuilder, configuration, "EdFi_Security", multiTenancyEnabled, isSqlServer); + } + + return services; + } + + private static void AddDatabaseHealthChecks( + IHealthChecksBuilder hcBuilder, + IConfigurationRoot configuration, + string connectionStringName, + bool multiTenancyEnabled, + bool isSqlServer + ) + { + Dictionary connectionStrings; + + if (multiTenancyEnabled) + { + var tenantSettings = + configuration.Get() + ?? throw new AdminApiException("Unable to load tenant configuration from appSettings"); + + connectionStrings = tenantSettings.Tenants.ToDictionary( + x => x.Key, + x => x.Value.ConnectionStrings[connectionStringName] + ); } else { - hcBuilder.AddNpgSql(connectionString); + connectionStrings = new() + { + { "SingleTenant", configuration.GetConnectionStringByName(connectionStringName) } + }; } - return services; + foreach (var connectionString in connectionStrings) + { + var healthCheckName = multiTenancyEnabled + ? $"{connectionString.Key}_{connectionStringName}" + : connectionStringName; + + if (isSqlServer) + { + hcBuilder.AddSqlServer(connectionString.Value, name: healthCheckName, tags: ["Databases"]); + } + else + { + hcBuilder.AddNpgSql(connectionString.Value, name: healthCheckName, tags: ["Databases"]); + } + } } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/IEnumerableExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/IEnumerableExtensions.cs index 5b2e7e8eb..22de7dd1f 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/IEnumerableExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/IEnumerableExtensions.cs @@ -5,6 +5,8 @@ using EdFi.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Settings; +using Microsoft.Extensions.Options; namespace EdFi.Ods.AdminApi.Infrastructure.Helpers; @@ -13,7 +15,7 @@ public static class IEnumerableExtensions public static string ToCommaSeparated(this IEnumerable vendorNamespacePrefixes) { return vendorNamespacePrefixes != null && vendorNamespacePrefixes.Any() - ? ToDelimiterSeparated(vendorNamespacePrefixes.Select(x => x.NamespacePrefix)) + ? ToDelimiterSeparated([.. vendorNamespacePrefixes.Select(x => x.NamespacePrefix).OrderBy(f => f)]) : string.Empty; } @@ -21,8 +23,25 @@ public static string ToDelimiterSeparated(this IEnumerable inputStrings, { var listOfStrings = inputStrings.ToList(); - return listOfStrings.Any() + return listOfStrings.Count != 0 ? string.Join(separator, listOfStrings) : string.Empty; + } + + public static IEnumerable Paginate(this IEnumerable source, int? offset, int? limit, IOptions settings) + { + try + { + offset ??= settings.Value.DefaultPageSizeOffset; + + limit ??= settings.Value.DefaultPageSizeLimit; + + return source.Skip(offset.Value).Take(limit.Value); + } + catch (Exception) + { + // If this throws an exception simply don't paginate. + return source; + } } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/JsonContractResolvers/ShouldSerializeContractResolver.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/JsonContractResolvers/ShouldSerializeContractResolver.cs deleted file mode 100644 index 28e2f9f37..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/JsonContractResolvers/ShouldSerializeContractResolver.cs +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Ods.AdminApi.Features.ClaimSets; -using Newtonsoft.Json.Serialization; -using Newtonsoft.Json; -using System.Reflection; - -namespace EdFi.Ods.AdminApi.Infrastructure.JsonContractResolvers; - -public class ShouldSerializeContractResolver : DefaultContractResolver -{ - private readonly IOdsSecurityModelVersionResolver _odsSecurityModelResolver; - - public ShouldSerializeContractResolver(IOdsSecurityModelVersionResolver resolver) : base() - { - _odsSecurityModelResolver = resolver; - } - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - JsonProperty property = base.CreateProperty(member, memberSerialization); - - if (property.DeclaringType == typeof(ResourceClaimModel) && (property.PropertyName is not null && property.PropertyName.ToLowerInvariant() == "readchanges")) - { - property.ShouldSerialize = - instance => - { - var securityModel = _odsSecurityModelResolver.DetermineSecurityModel(); - return securityModel is EdFiOdsSecurityModelCompatibility.Six or - EdFiOdsSecurityModelCompatibility.FiveThreeCqe; - }; - } - property.PropertyName = char.ToLowerInvariant(property.PropertyName![0]) + property.PropertyName[1..]; - return property; - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/OdsSecurityVersionResolver.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/OdsSecurityVersionResolver.cs deleted file mode 100644 index 35e6ed6f6..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/OdsSecurityVersionResolver.cs +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System.Runtime.CompilerServices; -using log4net; - -namespace EdFi.Ods.AdminApi.Infrastructure; - -public interface IOdsSecurityModelVersionResolver -{ - EdFiOdsSecurityModelCompatibility DetermineSecurityModel(); -} - -public class OdsSecurityVersionResolver : IOdsSecurityModelVersionResolver -{ - private readonly ILog _log; - private readonly Lazy _modelVersion; - private readonly string _odsApiVersion; - - public OdsSecurityVersionResolver(string odsApiVersion) - { - _odsApiVersion = odsApiVersion; - _log = LogManager.GetLogger(typeof(OdsSecurityVersionResolver)); - _modelVersion = new Lazy(InitializeModelVersion); - } - - public EdFiOdsSecurityModelCompatibility DetermineSecurityModel() => _modelVersion.Value; - - private EdFiOdsSecurityModelCompatibility InitializeModelVersion() - { - try - { - return _odsApiVersion switch - { - "5.3" => EdFiOdsSecurityModelCompatibility.ThreeThroughFive, - "5.3-cqe" => EdFiOdsSecurityModelCompatibility.FiveThreeCqe, - "6.0" or "6.1" => EdFiOdsSecurityModelCompatibility.Six, - _ => throw new SwitchExpressionException() - }; - } - catch (SwitchExpressionException) - { - _log.Error("OdsApiVersion not configured. Valid values are 5.3, 5.3-cqe, 6.0 and 6.1"); - throw new Exception("OdsApiVersion not configured. Valid values are 5.3, 5.3-cqe, 6.0 and 6.1"); - } - } -} - -public class EdFiOdsSecurityModelCompatibilityException : NotImplementedException -{ - public EdFiOdsSecurityModelCompatibilityException() - : base("Handling for security model not implemented") { } - public EdFiOdsSecurityModelCompatibilityException(EdFiOdsSecurityModelCompatibility version) - : base($"Handling for security model for version {version} not implemented") { } - public EdFiOdsSecurityModelCompatibilityException(string message) : base(message) { } -} - -public enum EdFiOdsSecurityModelCompatibility -{ - ThreeThroughFive = 1, - Six = 2, - Both = 3, - FiveThreeCqe = 4 -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Security/SecurityConstants.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Security/SecurityConstants.cs deleted file mode 100644 index 5c9717a2f..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Security/SecurityConstants.cs +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -namespace EdFi.Ods.AdminApi.Infrastructure.Security; - -public static class SecurityConstants -{ - public const string TokenEndpoint = "connect/token"; - public const string RegisterEndpoint = "connect/register"; - - public static class Scopes - { - public const string AdminApiFullAccess = "edfi_admin_api/full_access"; - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Security/SecurityExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Security/SecurityExtensions.cs index ca88c439f..ec8d4b9a6 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Security/SecurityExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Security/SecurityExtensions.cs @@ -3,38 +3,49 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Common.Infrastructure.Security; using EdFi.Ods.AdminApi.Features.Connect; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; +using OpenIddict.Server; +using static OpenIddict.Server.OpenIddictServerEvents; namespace EdFi.Ods.AdminApi.Infrastructure.Security; public static class SecurityExtensions { - public static void AddSecurityUsingOpenIddict(this IServiceCollection services, - IConfiguration configuration, IWebHostEnvironment webHostEnvironment) + public static void AddSecurityUsingOpenIddict( + this IServiceCollection services, + IConfiguration configuration, + IWebHostEnvironment webHostEnvironment + ) { + var issuer = configuration.Get("Authentication:IssuerUrl"); - var issuer = configuration.GetValue("Authentication:IssuerUrl"); - var authority = configuration.GetValue("Authentication:Authority"); - var isDockerEnvironment = configuration.GetValue("EnableDockerEnvironment"); + var isDockerEnvironment = configuration.Get("EnableDockerEnvironment"); //OpenIddict Server - var signingKeyValue = configuration.GetValue("Authentication:SigningKey"); - var signingKey = string.IsNullOrEmpty(signingKeyValue) ? null : new SymmetricSecurityKey(Convert.FromBase64String(signingKeyValue)); - - services.AddOpenIddict() + var signingKeyValue = configuration.Get("Authentication:SigningKey"); + var signingKey = string.IsNullOrEmpty(signingKeyValue) + ? null + : new SymmetricSecurityKey(Convert.FromBase64String(signingKeyValue)); + var validateIssuerSigningKey = configuration.Get("Authentication:ValidateIssuerSigningKey"); + services + .AddOpenIddict() .AddCore(opt => { - opt.UseEntityFrameworkCore().UseDbContext() + opt.UseEntityFrameworkCore() + .UseDbContext() .ReplaceDefaultEntities(); }) .AddServer(opt => { opt.AllowClientCredentialsFlow(); - + opt.SetAccessTokenLifetime(TimeSpan.FromMinutes(30)); opt.SetTokenEndpointUris(SecurityConstants.TokenEndpoint); opt.AddEphemeralEncryptionKey(); @@ -46,17 +57,25 @@ public static void AddSecurityUsingOpenIddict(this IServiceCollection services, { if (signingKey == null) { - throw new Exception("Invalid Configuration: Authentication:SigningKey is required."); + throw new AdminApiException("Invalid Configuration: Authentication:SigningKey is required."); } opt.AddSigningKey(signingKey); } - - opt.RegisterScopes(SecurityConstants.Scopes.AdminApiFullAccess); + foreach (var scope in SecurityConstants.Scopes.AllScopes) + { + opt.RegisterScopes(scope.Scope); + } var aspNetCoreBuilder = opt.UseAspNetCore().EnableTokenEndpointPassthrough(); if (isDockerEnvironment) { aspNetCoreBuilder.DisableTransportSecurityRequirement(); } + + opt.AddEventHandler(builder => + builder + .UseSingletonHandler() + .SetType(OpenIddictServerHandlerType.Custom) + ); }) .AddValidation(options => { @@ -66,32 +85,147 @@ public static void AddSecurityUsingOpenIddict(this IServiceCollection services, }); //Application Security - services.AddAuthentication(opt => - { - opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }).AddJwtBearer(opt => + var authenticationBuilder = services. + AddAuthentication(opt => + { + opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }); + + var roleClaimType = configuration.GetValue("Authentication:RoleClaimAttribute") + ?? SecurityConstants.DefaultRoleClaimType; + + authenticationBuilder.AddJwtBearer(opt => { - opt.Authority = authority; + opt.Authority = issuer; opt.SaveToken = true; opt.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, + ValidateIssuer = true, + ValidateIssuerSigningKey = validateIssuerSigningKey, ValidIssuer = issuer, + RoleClaimType = roleClaimType, IssuerSigningKey = signingKey }; - opt.RequireHttpsMetadata = !isDockerEnvironment; + opt.RequireHttpsMetadata = false; + opt.Events = new JwtBearerEvents + { + OnTokenValidated = context => + { + Console.WriteLine("Token validated successfully."); + + return Task.CompletedTask; + }, + OnAuthenticationFailed = context => + { + Console.WriteLine($"Authentication failed: {context.Exception.Message}"); + return Task.CompletedTask; + } + }; + }) + + // Named scheme for external Identity provider support + .AddJwtBearer("IdentityProvider", options => + { + options.Authority = issuer; + options.SaveToken = true; + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = true, + ValidateIssuerSigningKey = validateIssuerSigningKey, + ValidIssuer = issuer, + RoleClaimType = roleClaimType + }; }); + services.AddAuthorization(opt => { opt.DefaultPolicy = new AuthorizationPolicyBuilder() - .RequireClaim(OpenIddictConstants.Claims.Scope, SecurityConstants.Scopes.AdminApiFullAccess) + .AddAuthenticationSchemes() + .RequireAssertion(context => + !context.HasSucceeded + && context.User.HasClaim(c + => c.Type == OpenIddictConstants.Claims.Scope + && c.Value.Split(' ') + .ToList() + .Exists(scopeValue + => string.Equals(scopeValue, AuthorizationPolicies.DefaultScopePolicy.Scope, StringComparison.OrdinalIgnoreCase) + ) + ) + ) .Build(); + foreach (var scope in AuthorizationPolicies.ScopePolicies) + { + opt.AddPolicy(scope.PolicyName, policy => + { + policy.RequireAssertion(context => + { + if (context.User.HasClaim(c => c.Type == OpenIddictConstants.Claims.Scope)) + { + var scopes = context.User.FindFirst(c => c.Type == OpenIddictConstants.Claims.Scope)?.Value + .Split(' ') + .ToList(); + return scopes != null && (scopes.Contains(SecurityConstants.Scopes.AdminApiFullAccess.Scope, StringComparer.OrdinalIgnoreCase) + || scopes.Contains(scope.Scope, StringComparer.OrdinalIgnoreCase)); + } + return false; + }); + }); + } }); + services.AddControllers(); //Security Endpoints services.AddTransient(); services.AddTransient(); - services.AddControllers(); + } + public class DefaultTokenResponseHandler : IOpenIddictServerHandler + { + private const string DENIED_AUTHENTICATION_MESSAGE = + "Access Denied. Please review your information and try again."; + public ValueTask HandleAsync(ApplyTokenResponseContext context) + { + var response = context.Response; + + // For invalid_scope errors, set content type to application/problem+json + if (string.Equals(response.Error, OpenIddictConstants.Errors.InvalidScope, StringComparison.Ordinal)) + { + response.ErrorUri = ""; + response.ErrorDescription = "The request is missing required scope claims or has invalid scope values"; + + // Mark this response to be processed as problem+json + context.Transaction.SetProperty("CustomContentType", "application/problem+json"); + + return default; + } + + if ( + string.Equals( + response.Error, + OpenIddictConstants.Errors.InvalidGrant, + StringComparison.Ordinal + ) + || string.Equals( + response.Error, + OpenIddictConstants.Errors.UnsupportedGrantType, + StringComparison.Ordinal + ) + || string.Equals( + response.Error, + OpenIddictConstants.Errors.InvalidClient, + StringComparison.Ordinal + ) + ) + { + response.Error = OpenIddictConstants.Errors.InvalidClient; + response.ErrorDescription = DENIED_AUTHENTICATION_MESSAGE; + response.ErrorUri = ""; + } + + return default; + } } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Security/TokenEndpointBodyDescriptionFilter.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Security/TokenEndpointBodyDescriptionFilter.cs index 99b9b167c..dca9e9f34 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Security/TokenEndpointBodyDescriptionFilter.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Security/TokenEndpointBodyDescriptionFilter.cs @@ -35,9 +35,9 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) Type = "object", Properties = new Dictionary { - { "client_id", new OpenApiSchema { Type = "string "} }, - { "client_secret", new OpenApiSchema { Type = "string "} }, - { "grant_type", new OpenApiSchema { Type = "string "} }, + { "client_id", new OpenApiSchema { Type = "string"} }, + { "client_secret", new OpenApiSchema { Type = "string"} }, + { "grant_type", new OpenApiSchema { Type = "string"} }, { "scope", new OpenApiSchema { Type = "string"} }, } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/Action.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/Action.cs index 3b545e4ab..9b26a42cc 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/Action.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/Action.cs @@ -11,7 +11,6 @@ public class Action : Enumeration public static readonly Action Read = new("Read", "Read"); public static readonly Action Update = new("Update", "Update"); public static readonly Action Delete = new("Delete", "Delete"); - public static readonly Action ReadChanges = new("ReadChanges", "ReadChanges"); private Action(string value, string displayName) : base(value, displayName) { } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommand.cs index 7018ace2b..0c0303c03 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommand.cs @@ -3,33 +3,30 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Security.DataAccess.Contexts; namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; public class AddClaimSetCommand { - private readonly IOdsSecurityModelVersionResolver _resolver; - private readonly AddClaimSetCommandV53Service _v53Service; - private readonly AddClaimSetCommandV6Service _v6Service; + private readonly ISecurityContext _context; - public AddClaimSetCommand(IOdsSecurityModelVersionResolver resolver, - AddClaimSetCommandV53Service v53Service, - AddClaimSetCommandV6Service v6Service) + public AddClaimSetCommand(ISecurityContext context) { - _resolver = resolver; - _v53Service = v53Service; - _v6Service = v6Service; + _context = context; } public int Execute(IAddClaimSetModel claimSet) { - var securityModel = _resolver.DetermineSecurityModel(); - - return securityModel switch + var newClaimSet = new EdFi.Security.DataAccess.Models.ClaimSet { - EdFiOdsSecurityModelCompatibility.ThreeThroughFive or EdFiOdsSecurityModelCompatibility.FiveThreeCqe => _v53Service.Execute(claimSet), - EdFiOdsSecurityModelCompatibility.Six => _v6Service.Execute(claimSet), - _ => throw new EdFiOdsSecurityModelCompatibilityException(securityModel), + ClaimSetName = claimSet.ClaimSetName, + IsEdfiPreset = false, + ForApplicationUseOnly = false }; + _context.ClaimSets.Add(newClaimSet); + _context.SaveChanges(); + + return newClaimSet.ClaimSetId; } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommandV53Service.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommandV53Service.cs deleted file mode 100644 index 65402718e..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddClaimSetCommandV53Service.cs +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using ClaimSetEntity = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet; -using ISecurityContext = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts.ISecurityContext; - -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; - -public class AddClaimSetCommandV53Service -{ - private readonly ISecurityContext _context; - - public AddClaimSetCommandV53Service(ISecurityContext context) - { - _context = context; - } - - public int Execute(IAddClaimSetModel claimSet) - { - var newClaimSet = new ClaimSetEntity - { - ClaimSetName = claimSet.ClaimSetName, - Application = _context.Applications.Single() - }; - _context.ClaimSets.Add(newClaimSet); - _context.SaveChanges(); - - return newClaimSet.ClaimSetId; - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddOrEditResourcesOnClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddOrEditResourcesOnClaimSetCommand.cs index b7b4c2796..dc3d64f5c 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddOrEditResourcesOnClaimSetCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AddOrEditResourcesOnClaimSetCommand.cs @@ -3,11 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.Extensions; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor.Extensions; -using FluentValidation; namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; @@ -37,15 +33,11 @@ public void Execute(int claimSetId, List resources) resources.AddRange(childResources); var currentResources = resources.Select(r => { - var resource = allResources.FirstOrDefault(dr => (dr.Name ?? string.Empty).Equals(r.Name, StringComparison.Ordinal)); + var resource = allResources.Find(dr => (dr.Name ?? string.Empty).Equals(r.Name, StringComparison.Ordinal)); if (resource != null) { - resource.Create = r.Create; - resource.Read = r.Read; - resource.Update = r.Update; - resource.Delete = r.Delete; - resource.ReadChanges = r.ReadChanges; - resource.AuthStrategyOverridesForCRUD = r.AuthStrategyOverridesForCRUD; + resource.Actions = r.Actions; + resource.AuthorizationStrategyOverridesForCRUD = r.AuthorizationStrategyOverridesForCRUD; } return resource; }).ToList(); @@ -62,44 +54,44 @@ public void Execute(int claimSetId, List resources) _editResourceOnClaimSetCommand.Execute(editResourceModel); - if (resource!.AuthStrategyOverridesForCRUD != null && resource.AuthStrategyOverridesForCRUD.Any()) + if (resource!.AuthorizationStrategyOverridesForCRUD != null && resource.AuthorizationStrategyOverridesForCRUD.Any()) { var overrideAuthStrategyModel = new OverrideAuthorizationStrategyModel { ClaimSetId = claimSetId, ResourceClaimId = resource.Id, - AuthorizationStrategyForCreate = AuthStrategyOverrideForAction(resource.AuthStrategyOverridesForCRUD.Create()), - AuthorizationStrategyForRead = AuthStrategyOverrideForAction(resource.AuthStrategyOverridesForCRUD.Read()), - AuthorizationStrategyForUpdate = AuthStrategyOverrideForAction(resource.AuthStrategyOverridesForCRUD.Update()), - AuthorizationStrategyForDelete = AuthStrategyOverrideForAction(resource.AuthStrategyOverridesForCRUD.Delete()), - AuthorizationStrategyForReadChanges = AuthStrategyOverrideForAction(resource.AuthStrategyOverridesForCRUD.ReadChanges()) + ClaimSetResourceClaimActionAuthStrategyOverrides = resource.AuthorizationStrategyOverridesForCRUD }; _overrideDefaultAuthorizationStrategyCommand.Execute(overrideAuthStrategyModel); } } - - static int[] AuthStrategyOverrideForAction(ClaimSetResourceClaimActionAuthStrategies? claimSetResourceClaimActionAuthStrategies) - { - if (claimSetResourceClaimActionAuthStrategies != null && claimSetResourceClaimActionAuthStrategies.AuthorizationStrategies != null) - { - return claimSetResourceClaimActionAuthStrategies.AuthorizationStrategies.Where(p => p is not null).Select(p => p!.AuthStrategyId).ToArray(); - } - return Array.Empty(); - } } private List GetDbResources() { var allResources = new List(); var parentResources = _getResourceClaimsQuery.Execute().ToList(); - allResources.AddRange(parentResources); - foreach (var children in parentResources.Select(x => x.Children)) + + foreach (var resource in parentResources) { - allResources.AddRange(children); + AddResourceWithChildren(resource, allResources); } return allResources; } + + private void AddResourceWithChildren(ResourceClaim resource, List allResources) + { + allResources.Add(resource); + + if (resource.Children != null && resource.Children.Any()) + { + foreach (var child in resource.Children) + { + AddResourceWithChildren(child, allResources); + } + } + } } public class AddClaimSetModel : IAddClaimSetModel @@ -116,11 +108,6 @@ public class EditResourceOnClaimSetModel : IEditResourceOnClaimSetModel public class OverrideAuthorizationStrategyModel : IOverrideDefaultAuthorizationStrategyModel { public int ClaimSetId { get; set; } - public int ResourceClaimId { get; set; } - public int[]? AuthorizationStrategyForCreate { get; set; } - public int[]? AuthorizationStrategyForRead { get; set; } - public int[]? AuthorizationStrategyForUpdate { get; set; } - public int[]? AuthorizationStrategyForDelete { get; set; } - public int[]? AuthorizationStrategyForReadChanges { get; set; } - + public int ResourceClaimId { get; set; } + public List? ClaimSetResourceClaimActionAuthStrategyOverrides { get; set; } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AuthStrategyResolver.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AuthStrategyResolver.cs index 31d9f5205..b058d3c4d 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AuthStrategyResolver.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AuthStrategyResolver.cs @@ -23,56 +23,29 @@ public AuthStrategyResolver(ISecurityContext securityContext) public IEnumerable ResolveAuthStrategies(IEnumerable resourceClaims) { - var dbAuthStrategies = _securityContext.AuthorizationStrategies; + var dbAuthStrategies = _securityContext.AuthorizationStrategies.ToList(); foreach (var claim in resourceClaims) { - if (claim.DefaultAuthStrategiesForCRUD != null && claim.DefaultAuthStrategiesForCRUD.Any()) + if (claim.AuthorizationStrategyOverridesForCRUD != null && claim.AuthorizationStrategyOverridesForCRUD.Any()) { - foreach (var claimSetResourceClaimActionAuthStrategyItem in claim.DefaultAuthStrategiesForCRUD.Where(x => x != null)) + foreach (var authStrategyOverride in claim.AuthorizationStrategyOverridesForCRUD.Where(x => x != null)) { - if (claimSetResourceClaimActionAuthStrategyItem != null && claimSetResourceClaimActionAuthStrategyItem.AuthorizationStrategies != null) - { - foreach (var authorizationStrategyItem in claimSetResourceClaimActionAuthStrategyItem.AuthorizationStrategies) - { - if (authorizationStrategyItem is null) continue; - - var authStrategy = dbAuthStrategies.AsEnumerable().SingleOrDefault( - x => x.AuthorizationStrategyName.Equals( - authorizationStrategyItem.AuthStrategyName, StringComparison.InvariantCultureIgnoreCase)); - - if (authStrategy != null) - { - authorizationStrategyItem.AuthStrategyId = authStrategy.AuthorizationStrategyId; - authorizationStrategyItem.DisplayName = authStrategy.DisplayName; - } - } - } - - - } - } - - if (claim.AuthStrategyOverridesForCRUD != null && claim.AuthStrategyOverridesForCRUD.Any()) - { - foreach (var authStrategyOverride in claim.AuthStrategyOverridesForCRUD.Where(x => x != null)) - { - if (authStrategyOverride != null && authStrategyOverride.AuthorizationStrategies != null) - { - foreach (var authorizationStrategyItem in authStrategyOverride.AuthorizationStrategies) - { - if (authorizationStrategyItem is null) continue; - - var authStrategy = dbAuthStrategies.AsEnumerable().SingleOrDefault( - x => x.AuthorizationStrategyName.Equals( - authorizationStrategyItem.AuthStrategyName, - StringComparison.InvariantCultureIgnoreCase)); - - if (authStrategy != null) - { - authorizationStrategyItem.AuthStrategyId = authStrategy.AuthorizationStrategyId; - authorizationStrategyItem.DisplayName = authStrategy.DisplayName; - } + if (authStrategyOverride is null) continue; + if (authStrategyOverride.AuthorizationStrategies != null) + { + foreach (var strategy in authStrategyOverride.AuthorizationStrategies) + { + var authStrategy = dbAuthStrategies.SingleOrDefault( + x => x.AuthorizationStrategyName.Equals( + strategy.AuthStrategyName, + StringComparison.InvariantCultureIgnoreCase)); + + if (authStrategy != null) + { + strategy.AuthStrategyId = authStrategy.AuthorizationStrategyId; + strategy.AuthStrategyName = authStrategy.AuthorizationStrategyName; + } } } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AuthorizationStrategy.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AuthorizationStrategy.cs index d923dea69..672613f3f 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AuthorizationStrategy.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/AuthorizationStrategy.cs @@ -3,12 +3,14 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using Swashbuckle.AspNetCore.Annotations; + namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; +[SwaggerSchema(Title = "ResourceClaimAuthorizationStrategy")] public class AuthorizationStrategy { public int AuthStrategyId { get; set; } public string? AuthStrategyName { get; set; } - public string? DisplayName { get; set; } public bool IsInheritedFromParent { get; set; } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/ClaimSetResourceClaimActionAuthStrategies.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/ClaimSetResourceClaimActionAuthStrategies.cs index 720545947..fe915a86a 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/ClaimSetResourceClaimActionAuthStrategies.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/ClaimSetResourceClaimActionAuthStrategies.cs @@ -1,20 +1,23 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; -using Swashbuckle.AspNetCore.Annotations; - -namespace EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; - -public interface IClaimSetResourceClaimActionAuthStrategies -{ - IList AuthorizationStrategies { get; } -} - -[SwaggerSchema(Title = "ClaimSetResourceClaimActionAuthorizationStrategies")] -public class ClaimSetResourceClaimActionAuthStrategies : IClaimSetResourceClaimActionAuthStrategies -{ - public IList AuthorizationStrategies { get; set; } = new List(); -} +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; + +public interface IClaimSetResourceClaimActionAuthStrategies +{ + int? ActionId { get; } + string? ActionName { get; } + IEnumerable? AuthorizationStrategies { get; } +} + +[SwaggerSchema(Title = "ClaimSetResourceClaimActionAuthorizationStrategies")] +public class ClaimSetResourceClaimActionAuthStrategies : IClaimSetResourceClaimActionAuthStrategies +{ + public int? ActionId { get; set; } + public string? ActionName { get; set; } + public IEnumerable? AuthorizationStrategies { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/CopyClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/CopyClaimSetCommand.cs new file mode 100644 index 000000000..afb848acb --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/CopyClaimSetCommand.cs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Security.DataAccess.Contexts; +using EdFi.Security.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApp.Management.ClaimSetEditor +{ + public interface ICopyClaimSetCommand + { + int Execute(ICopyClaimSetModel claimSet); + } + + public class CopyClaimSetCommand : ICopyClaimSetCommand + { + private readonly ISecurityContext _context; + + public CopyClaimSetCommand(ISecurityContext context) + { + _context = context; + } + + public int Execute(ICopyClaimSetModel claimSet) + { + var newClaimSet = new ClaimSet + { + ClaimSetName = claimSet.Name, + IsEdfiPreset = false, + ForApplicationUseOnly = false + }; + + var originalResourceClaims = + _context.ClaimSetResourceClaimActions + .Where(x => x.ClaimSet.ClaimSetId == claimSet.OriginalId) + .Include(x => x.ResourceClaim) + .Include(x => x.Action) + .Include(x => x.AuthorizationStrategyOverrides) + .ThenInclude(x => x.AuthorizationStrategy) + .ToList(); + _context.ClaimSets.Add(newClaimSet); + + foreach (var resourceClaim in originalResourceClaims.ToList()) + { + List? authStrategyOverrides = null; + if (resourceClaim.AuthorizationStrategyOverrides != null && resourceClaim.AuthorizationStrategyOverrides.Any()) + { + authStrategyOverrides = new List(); + foreach (var authStrategyOverride in resourceClaim.AuthorizationStrategyOverrides) + { + authStrategyOverrides.Add(new ClaimSetResourceClaimActionAuthorizationStrategyOverrides + { AuthorizationStrategy = authStrategyOverride.AuthorizationStrategy }); + } + } + var copyResourceClaim = new ClaimSetResourceClaimAction + { + ClaimSet = newClaimSet, + Action = resourceClaim.Action, + AuthorizationStrategyOverrides = authStrategyOverrides, + ResourceClaim = resourceClaim.ResourceClaim + }; + _context.ClaimSetResourceClaimActions.Add(copyResourceClaim); + } + _context.SaveChanges(); + + return newClaimSet.ClaimSetId; + } + } + + public interface ICopyClaimSetModel + { + string? Name { get; } + int OriginalId { get; } + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommand.cs index 8b45b8f88..d96f7d364 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommand.cs @@ -3,7 +3,8 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Security.DataAccess.Contexts; namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; @@ -14,34 +15,34 @@ public interface IDeleteClaimSetCommand public class DeleteClaimSetCommand : IDeleteClaimSetCommand { - private readonly IOdsSecurityModelVersionResolver _resolver; - private readonly DeleteClaimSetCommandV53Service _v53Service; - private readonly DeleteClaimSetCommandV6Service _v6Service; + private readonly ISecurityContext _context; - public DeleteClaimSetCommand(IOdsSecurityModelVersionResolver resolver, - DeleteClaimSetCommandV53Service v53Service, - DeleteClaimSetCommandV6Service v6Service) + public DeleteClaimSetCommand(ISecurityContext context) { - _resolver = resolver; - _v53Service = v53Service; - _v6Service = v6Service; + _context = context; } public void Execute(IDeleteClaimSetModel claimSet) { - var securityModel = _resolver.DetermineSecurityModel(); + var claimSetToDelete = _context.ClaimSets.Single(x => x.ClaimSetId == claimSet.Id); + if (claimSetToDelete.ForApplicationUseOnly || claimSetToDelete.IsEdfiPreset) + { + throw new AdminApiException($"Claim set({claimSetToDelete.ClaimSetName}) is system reserved. Can not be deleted."); + } - switch (securityModel) + var resourceClaimsForClaimSetId = + _context.ClaimSetResourceClaimActions.Where(x => x.ClaimSet.ClaimSetId == claimSet.Id).ToList(); + foreach (var resourceClaimAction in resourceClaimsForClaimSetId) { - case EdFiOdsSecurityModelCompatibility.ThreeThroughFive or EdFiOdsSecurityModelCompatibility.FiveThreeCqe: - _v53Service.Execute(claimSet); - break; - case EdFiOdsSecurityModelCompatibility.Six: - _v6Service.Execute(claimSet); - break; - default: - throw new EdFiOdsSecurityModelCompatibilityException(securityModel); + var resourceClaimActionAuthorizationStrategyOverrides = _context.ClaimSetResourceClaimActionAuthorizationStrategyOverrides. + Where(x => x.ClaimSetResourceClaimActionId == resourceClaimAction.ClaimSetResourceClaimActionId); + + _context.ClaimSetResourceClaimActionAuthorizationStrategyOverrides.RemoveRange(resourceClaimActionAuthorizationStrategyOverrides); } + + _context.ClaimSetResourceClaimActions.RemoveRange(resourceClaimsForClaimSetId); + _context.ClaimSets.Remove(claimSetToDelete); + _context.SaveChanges(); } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommandV53Service.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommandV53Service.cs deleted file mode 100644 index eb5b534ee..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteClaimSetCommandV53Service.cs +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; - -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; - -public class DeleteClaimSetCommandV53Service -{ - private readonly ISecurityContext _context; - - public DeleteClaimSetCommandV53Service(ISecurityContext context) - { - _context = context; - } - - public void Execute(IDeleteClaimSetModel claimSet) - { - var claimSetToDelete = _context.ClaimSets.Single(x => x.ClaimSetId == claimSet.Id); - - if (Constants.DefaultClaimSets.Contains(claimSetToDelete.ClaimSetName) || - Constants.SystemReservedClaimSets.Contains(claimSetToDelete.ClaimSetName)) - { - throw new AdminApiException($"Claim set({claimSetToDelete.ClaimSetName}) is system reserved.Can not be deleted."); - } - - var resourceClaimsForClaimSetId = - _context.ClaimSetResourceClaims.Where(x => x.ClaimSet.ClaimSetId == claimSet.Id).ToList(); - _context.ClaimSetResourceClaims.RemoveRange(resourceClaimsForClaimSetId); - _context.ClaimSets.Remove(claimSetToDelete); - _context.SaveChanges(); - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteResourceClaimOnClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteResourceClaimOnClaimSetCommand.cs new file mode 100644 index 000000000..b9a6274de --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/DeleteResourceClaimOnClaimSetCommand.cs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Security.DataAccess.Contexts; + +namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; + +public interface IDeleteResouceClaimOnClaimSetCommand +{ + void Execute(int claimSetId, int resourceClaimId); +} + +public class DeleteResouceClaimOnClaimSetCommand : IDeleteResouceClaimOnClaimSetCommand +{ + private readonly ISecurityContext _context; + + public DeleteResouceClaimOnClaimSetCommand(ISecurityContext context) + { + _context = context; + } + + public void Execute(int claimSetId, int resourceClaimId) + { + var resourceClaimsForClaimSetId = + _context.ClaimSetResourceClaimActions.Where(x => x.ClaimSetId == claimSetId && x.ResourceClaimId == resourceClaimId).ToList(); + foreach (var resourceClaimAction in resourceClaimsForClaimSetId) + { + var resourceClaimActionAuthorizationStrategyOverrides = _context.ClaimSetResourceClaimActionAuthorizationStrategyOverrides. + Where(x => x.ClaimSetResourceClaimActionId == resourceClaimAction.ClaimSetResourceClaimActionId); + + _context.ClaimSetResourceClaimActionAuthorizationStrategyOverrides.RemoveRange(resourceClaimActionAuthorizationStrategyOverrides); + } + + _context.ClaimSetResourceClaimActions.RemoveRange(resourceClaimsForClaimSetId); + _context.SaveChanges(); + } + +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommand.cs index d79b5ae8f..c1001e312 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommand.cs @@ -3,6 +3,10 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Security.DataAccess.Contexts; + namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor { public interface IEditClaimSetCommand @@ -12,29 +16,47 @@ public interface IEditClaimSetCommand public class EditClaimSetCommand : IEditClaimSetCommand { - private readonly IOdsSecurityModelVersionResolver _resolver; - private readonly EditClaimSetCommandV53Service _v53Service; - private readonly EditClaimSetCommandV6Service _v6Service; + private readonly ISecurityContext _securityContext; + private readonly IUsersContext _usersContext; - public EditClaimSetCommand(IOdsSecurityModelVersionResolver resolver, - EditClaimSetCommandV53Service v53Service, - EditClaimSetCommandV6Service v6Service) + public EditClaimSetCommand(ISecurityContext securityContext, IUsersContext usersContext) { - _resolver = resolver; - _v53Service = v53Service; - _v6Service = v6Service; + _securityContext = securityContext; + _usersContext = usersContext; } public int Execute(IEditClaimSetModel claimSet) { - var securityModel = _resolver.DetermineSecurityModel(); + var existingClaimSet = _securityContext.ClaimSets.Single(x => x.ClaimSetId == claimSet.ClaimSetId); + + if (existingClaimSet.ForApplicationUseOnly || existingClaimSet.IsEdfiPreset) + { + throw new AdminApiException($"Claim set ({existingClaimSet.ClaimSetName}) is system reserved. May not be modified."); + } - return securityModel switch + if (claimSet.ClaimSetName is null) throw new InvalidOperationException("Cannot have a null ClaimSetName"); + if (!claimSet.ClaimSetName.Equals(existingClaimSet.ClaimSetName, StringComparison.InvariantCultureIgnoreCase)) { - EdFiOdsSecurityModelCompatibility.ThreeThroughFive or EdFiOdsSecurityModelCompatibility.FiveThreeCqe => _v53Service.Execute(claimSet), - EdFiOdsSecurityModelCompatibility.Six => _v6Service.Execute(claimSet), - _ => throw new EdFiOdsSecurityModelCompatibilityException(securityModel), - }; + ReAssociateApplicationsToRenamedClaimSet(existingClaimSet.ClaimSetName, claimSet.ClaimSetName); + } + + existingClaimSet.ClaimSetName = claimSet.ClaimSetName; + + _securityContext.SaveChanges(); + _usersContext.SaveChanges(); + + return existingClaimSet.ClaimSetId; + + void ReAssociateApplicationsToRenamedClaimSet(string existingClaimSetName, string newClaimSetName) + { + var associatedApplications = _usersContext.Applications + .Where(x => x.ClaimSetName == existingClaimSetName); + + foreach (var application in associatedApplications) + { + application.ClaimSetName = newClaimSetName; + } + } } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommandV53Service.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommandV53Service.cs deleted file mode 100644 index c0f5daf4e..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditClaimSetCommandV53Service.cs +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; -using EdFi.Admin.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; - -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; - -public class EditClaimSetCommandV53Service -{ - private readonly ISecurityContext _securityContext; - private readonly IUsersContext _usersContext; - - public EditClaimSetCommandV53Service(ISecurityContext securityContext, IUsersContext usersContext) - { - _securityContext = securityContext; - _usersContext = usersContext; - } - - public int Execute(IEditClaimSetModel claimSet) - { - var existingClaimSet = _securityContext.ClaimSets.Single(x => x.ClaimSetId == claimSet.ClaimSetId); - - if (Constants.DefaultClaimSets.Contains(existingClaimSet.ClaimSetName) || - Constants.SystemReservedClaimSets.Contains(existingClaimSet.ClaimSetName)) - { - throw new AdminApiException($"Claim set ({existingClaimSet.ClaimSetName}) is system reserved.May not be modified."); - } - - if (claimSet.ClaimSetName is null) throw new InvalidOperationException("Cannot have a null ClaimSetName"); - if (!claimSet.ClaimSetName.Equals(existingClaimSet.ClaimSetName, StringComparison.InvariantCultureIgnoreCase)) - { - ReAssociateApplicationsToRenamedClaimSet(existingClaimSet.ClaimSetName, claimSet.ClaimSetName); - } - - existingClaimSet.ClaimSetName = claimSet.ClaimSetName; - - void ReAssociateApplicationsToRenamedClaimSet(string existingClaimSetName, string newClaimSetName) - { - var associatedApplications = _usersContext.Applications - .Where(x => x.ClaimSetName == existingClaimSetName); - - foreach (var application in associatedApplications) - { - application.ClaimSetName = newClaimSetName; - } - } - - _securityContext.SaveChanges(); - _usersContext.SaveChanges(); - - return existingClaimSet.ClaimSetId; - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommand.cs index a62453bcd..7caf0fdb3 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommand.cs @@ -3,36 +3,90 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Security.DataAccess.Contexts; +using EdFi.Security.DataAccess.Models; +using Microsoft.EntityFrameworkCore; + namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; public class EditResourceOnClaimSetCommand { - private readonly IOdsSecurityModelVersionResolver _resolver; - private readonly EditResourceOnClaimSetCommandV53Service _v53Service; - private readonly EditResourceOnClaimSetCommandV6Service _v6Service; + private readonly ISecurityContext _context; - public EditResourceOnClaimSetCommand(IOdsSecurityModelVersionResolver resolver, - EditResourceOnClaimSetCommandV53Service v53Service, - EditResourceOnClaimSetCommandV6Service v6Service) + public EditResourceOnClaimSetCommand(ISecurityContext context) { - _resolver = resolver; - _v53Service = v53Service; - _v6Service = v6Service; + _context = context; } public void Execute(IEditResourceOnClaimSetModel model) { - var securityModel = _resolver.DetermineSecurityModel(); - switch (securityModel) + var resourceClaimToEdit = model.ResourceClaim; + if (resourceClaimToEdit is null) return; + + var claimSetToEdit = _context.ClaimSets.Single(x => x.ClaimSetId == model.ClaimSetId); + + var claimSetResourceClaimsToEdit = _context.ClaimSetResourceClaimActions + .Include(x => x.ResourceClaim) + .Include(x => x.Action) + .Include(x => x.ClaimSet) + .Where(x => x.ResourceClaim.ResourceClaimId == resourceClaimToEdit.Id && x.ClaimSet.ClaimSetId == claimSetToEdit.ClaimSetId) + .ToList(); + + AddEnabledActionsToClaimSet(resourceClaimToEdit, claimSetResourceClaimsToEdit, claimSetToEdit); + + RemoveDisabledActionsFromClaimSet(resourceClaimToEdit, claimSetResourceClaimsToEdit); + + _context.SaveChanges(); + } + + private void RemoveDisabledActionsFromClaimSet(ResourceClaim modelResourceClaim, IEnumerable resourceClaimsToEdit) + { + var recordsToRemove = new List(); + + foreach (var claimSetResourceClaim in resourceClaimsToEdit) + { + if(modelResourceClaim.Actions != null && + modelResourceClaim.Actions.Exists(x => x.Name != null && x.Name.Equals(claimSetResourceClaim.Action.ActionName, + StringComparison.InvariantCultureIgnoreCase) && !x.Enabled)) + { + recordsToRemove.Add(claimSetResourceClaim); + } + } + + if (recordsToRemove.Any()) + { + _context.ClaimSetResourceClaimActions.RemoveRange(recordsToRemove); + } + } + + private void AddEnabledActionsToClaimSet(ResourceClaim modelResourceClaim, + IReadOnlyCollection claimSetResourceClaimsToEdit, EdFi.Security.DataAccess.Models.ClaimSet claimSetToEdit) + { + var actionsFromDb = _context.Actions.ToList(); + + var resourceClaimFromDb = _context.ResourceClaims.Single(x => x.ResourceClaimId == modelResourceClaim.Id); + + var recordsToAdd = new List(); + + if(modelResourceClaim.Actions != null) + { + foreach (var action in modelResourceClaim.Actions) + { + if (action.Enabled && claimSetResourceClaimsToEdit.All(x => !x.Action.ActionName.Equals(action.Name, + StringComparison.InvariantCultureIgnoreCase))) + { + recordsToAdd.Add(new ClaimSetResourceClaimAction + { + Action = actionsFromDb.Single(x => x.ActionName.Equals(action.Name, StringComparison.InvariantCultureIgnoreCase)), + ClaimSet = claimSetToEdit, + ResourceClaim = resourceClaimFromDb + }); + } + } + } + if (recordsToAdd.Any()) { - case EdFiOdsSecurityModelCompatibility.ThreeThroughFive or EdFiOdsSecurityModelCompatibility.FiveThreeCqe: - _v53Service.Execute(model); - break; - case EdFiOdsSecurityModelCompatibility.Six: - _v6Service.Execute(model); - break; - default: - throw new EdFiOdsSecurityModelCompatibilityException(securityModel); + _context.ClaimSetResourceClaimActions.AddRange(recordsToAdd); } } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommandV53Service.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommandV53Service.cs deleted file mode 100644 index d71d78348..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/EditResourceOnClaimSetCommandV53Service.cs +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models; -using Microsoft.EntityFrameworkCore; - -using SecurityClaimSet = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ClaimSet; - -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; - -public class EditResourceOnClaimSetCommandV53Service -{ - private readonly ISecurityContext _context; - - public EditResourceOnClaimSetCommandV53Service(ISecurityContext context) - { - _context = context; - } - - public void Execute(IEditResourceOnClaimSetModel model) - { - var resourceClaimToEdit = model.ResourceClaim; - - if (resourceClaimToEdit is null) return; - - var claimSetToEdit = _context.ClaimSets.Single(x => x.ClaimSetId == model.ClaimSetId); - - var claimSetResourceClaimsToEdit = _context.ClaimSetResourceClaims - .Include(x => x.ResourceClaim) - .Include(x => x.Action) - .Include(x => x.ClaimSet) - .Where(x => x.ResourceClaim.ResourceClaimId == resourceClaimToEdit.Id && x.ClaimSet.ClaimSetId == claimSetToEdit.ClaimSetId) - .ToList(); - - AddEnabledActionsToClaimSet(resourceClaimToEdit, claimSetResourceClaimsToEdit, claimSetToEdit); - - RemoveDisabledActionsFromClaimSet(resourceClaimToEdit, claimSetResourceClaimsToEdit); - - _context.SaveChanges(); - } - - private void RemoveDisabledActionsFromClaimSet(ResourceClaim modelResourceClaim, IEnumerable resourceClaimsToEdit) - { - var recordsToRemove = new List(); - - foreach (var claimSetResourceClaim in resourceClaimsToEdit) - { - if (claimSetResourceClaim.Action.ActionName == Action.Create.Value && !modelResourceClaim.Create) - { - recordsToRemove.Add(claimSetResourceClaim); - } - else if (claimSetResourceClaim.Action.ActionName == Action.Read.Value && !modelResourceClaim.Read) - { - recordsToRemove.Add(claimSetResourceClaim); - } - else if (claimSetResourceClaim.Action.ActionName == Action.Update.Value && !modelResourceClaim.Update) - { - recordsToRemove.Add(claimSetResourceClaim); - } - else if (claimSetResourceClaim.Action.ActionName == Action.Delete.Value && !modelResourceClaim.Delete) - { - recordsToRemove.Add(claimSetResourceClaim); - } - else if (claimSetResourceClaim.Action.ActionName == Action.ReadChanges.Value && !modelResourceClaim.ReadChanges) - { - recordsToRemove.Add(claimSetResourceClaim); - } - } - - if (recordsToRemove.Any()) - { - _context.ClaimSetResourceClaims.RemoveRange(recordsToRemove); - } - } - - private void AddEnabledActionsToClaimSet(ResourceClaim modelResourceClaim, IReadOnlyCollection claimSetResourceClaimsToEdit, SecurityClaimSet claimSetToEdit) - { - var actionsFromDb = _context.Actions.ToList(); - - var resourceClaimFromDb = _context.ResourceClaims.Single(x => x.ResourceClaimId == modelResourceClaim.Id); - - var recordsToAdd = new List(); - - if (modelResourceClaim.Create && claimSetResourceClaimsToEdit.All(x => x.Action.ActionName != Action.Create.Value)) - { - recordsToAdd.Add(new ClaimSetResourceClaim - { - Action = actionsFromDb.Single(x => x.ActionName == Action.Create.Value), - ClaimSet = claimSetToEdit, - ResourceClaim = resourceClaimFromDb - }); - } - - if (modelResourceClaim.Read && claimSetResourceClaimsToEdit.All(x => x.Action.ActionName != Action.Read.Value)) - { - recordsToAdd.Add(new ClaimSetResourceClaim - { - Action = actionsFromDb.Single(x => x.ActionName == Action.Read.Value), - ClaimSet = claimSetToEdit, - ResourceClaim = resourceClaimFromDb - }); - } - - if (modelResourceClaim.Update && claimSetResourceClaimsToEdit.All(x => x.Action.ActionName != Action.Update.Value)) - { - recordsToAdd.Add(new ClaimSetResourceClaim - { - Action = actionsFromDb.Single(x => x.ActionName == Action.Update.Value), - ClaimSet = claimSetToEdit, - ResourceClaim = resourceClaimFromDb - }); - } - - if (modelResourceClaim.Delete && claimSetResourceClaimsToEdit.All(x => x.Action.ActionName != Action.Delete.Value)) - { - recordsToAdd.Add(new ClaimSetResourceClaim - { - Action = actionsFromDb.Single(x => x.ActionName == Action.Delete.Value), - ClaimSet = claimSetToEdit, - ResourceClaim = resourceClaimFromDb - }); - } - - if (modelResourceClaim.ReadChanges && claimSetResourceClaimsToEdit.All(x => x.Action.ActionName != Action.ReadChanges.Value)) - { - recordsToAdd.Add(new ClaimSetResourceClaim - { - Action = actionsFromDb.Single(x => x.ActionName == Action.ReadChanges.Value), - ClaimSet = claimSetToEdit, - ResourceClaim = resourceClaimFromDb - }); - } - - if (recordsToAdd.Any()) - { - _context.ClaimSetResourceClaims.AddRange(recordsToAdd); - } - } -} - diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/Extensions/AuthorizationStrategiesExtension.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/Extensions/AuthorizationStrategiesExtension.cs index e7f7d6386..211387f0d 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/Extensions/AuthorizationStrategiesExtension.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/Extensions/AuthorizationStrategiesExtension.cs @@ -9,27 +9,22 @@ public static class AuthorizationStrategiesExtension { public static AuthorizationStrategy? Create(this AuthorizationStrategy?[] authorizationStrategies) { - return authorizationStrategies.Length > 0 ? authorizationStrategies[0] : null; + return authorizationStrategies[0]; } public static AuthorizationStrategy? Read(this AuthorizationStrategy?[] authorizationStrategies) { - return authorizationStrategies.Length > 1 ? authorizationStrategies[1] : null; + return authorizationStrategies[1]; } public static AuthorizationStrategy? Update(this AuthorizationStrategy?[] authorizationStrategies) { - return authorizationStrategies.Length > 2 ? authorizationStrategies[2] : null; - } - + return authorizationStrategies[2]; + } + public static AuthorizationStrategy? Delete(this AuthorizationStrategy?[] authorizationStrategies) { - return authorizationStrategies.Length > 3 ? authorizationStrategies[3] : null; - } - - public static AuthorizationStrategy? ReadChanges(this AuthorizationStrategy?[] authorizationStrategies) - { - return authorizationStrategies.Length > 4 ? authorizationStrategies[4] : null; + return authorizationStrategies[3]; } public static AuthorizationStrategy?[] AddAuthorizationStrategyOverrides(this AuthorizationStrategy?[] authorizationStrategies, @@ -42,10 +37,8 @@ public static class AuthorizationStrategiesExtension else if (actionName == Action.Update.Value) authorizationStrategies[2] = strategy; else if (actionName == Action.Delete.Value) - authorizationStrategies[3] = strategy; - else if (actionName == Action.ReadChanges.Value) - authorizationStrategies[4] = strategy; + authorizationStrategies[3] = strategy; return authorizationStrategies; - } + } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllAuthorizationStrategiesQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllAuthorizationStrategiesQuery.cs index e9adeebf6..af703ba66 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllAuthorizationStrategiesQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllAuthorizationStrategiesQuery.cs @@ -27,8 +27,7 @@ public IReadOnlyList Execute() .Select(x => new AuthorizationStrategy { AuthStrategyId = x.AuthorizationStrategyId, - AuthStrategyName = x.AuthorizationStrategyName, - DisplayName = x.DisplayName + AuthStrategyName = x.AuthorizationStrategyName }).ToList(); } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQuery.cs deleted file mode 100644 index ecfc89b40..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQuery.cs +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using ClaimSet = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ClaimSet; - -namespace EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; - -public interface IGetAllClaimSetsQuery -{ - IReadOnlyList Execute(); - IReadOnlyList Execute(CommonQueryParams commonQueryParams); -} - -public class GetAllClaimSetsQuery : IGetAllClaimSetsQuery -{ - private readonly IOdsSecurityModelVersionResolver _resolver; - private readonly GetAllClaimSetsQueryV53Service _v53Service; - private readonly GetAllClaimSetsQueryV6Service _v6Service; - - public GetAllClaimSetsQuery(IOdsSecurityModelVersionResolver resolver, - GetAllClaimSetsQueryV53Service v53Service, - GetAllClaimSetsQueryV6Service v6Service) - { - _resolver = resolver; - _v53Service = v53Service; - _v6Service = v6Service; - } - - public IReadOnlyList Execute() - { - var securityModel = _resolver.DetermineSecurityModel(); - - return securityModel switch - { - EdFiOdsSecurityModelCompatibility.ThreeThroughFive or EdFiOdsSecurityModelCompatibility.FiveThreeCqe => _v53Service.Execute(), - EdFiOdsSecurityModelCompatibility.Six => _v6Service.Execute(), - _ => throw new EdFiOdsSecurityModelCompatibilityException(securityModel), - }; - } - - public IReadOnlyList Execute(CommonQueryParams commonQueryParams) - { - var securityModel = _resolver.DetermineSecurityModel(); - - return securityModel switch - { - EdFiOdsSecurityModelCompatibility.ThreeThroughFive or EdFiOdsSecurityModelCompatibility.FiveThreeCqe => _v53Service.Execute(commonQueryParams), - EdFiOdsSecurityModelCompatibility.Six => _v6Service.Execute(commonQueryParams), - _ => throw new EdFiOdsSecurityModelCompatibilityException(securityModel), - }; - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQueryV53Service.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQueryV53Service.cs deleted file mode 100644 index 28e0b0c03..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetAllClaimSetsQueryV53Service.cs +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Helpers; -using EdFi.Ods.AdminApi.Infrastructure.Extensions; -using Microsoft.Extensions.Options; -using ClaimSet = EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ClaimSet; - -namespace EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; - -public class GetAllClaimSetsQueryV53Service -{ - private readonly ISecurityContext _securityContext; - private readonly IOptions _options; - - public GetAllClaimSetsQueryV53Service(ISecurityContext securityContext, IOptions options) - { - _securityContext = securityContext; - _options = options; - } - - public IReadOnlyList Execute() - { - return _securityContext.ClaimSets - .Select(x => new ClaimSet - { - Id = x.ClaimSetId, - Name = x.ClaimSetName, - IsEditable = !Constants.DefaultClaimSets.Contains(x.ClaimSetName) && - !Constants.SystemReservedClaimSets.Contains(x.ClaimSetName) - }) - .Distinct() - .OrderBy(x => x.Name) - .ToList(); - } - - public IReadOnlyList Execute(CommonQueryParams commonQueryParams) - { - return _securityContext.ClaimSets - .Select(x => new ClaimSet - { - Id = x.ClaimSetId, - Name = x.ClaimSetName, - IsEditable = !Constants.DefaultClaimSets.Contains(x.ClaimSetName) && - !Constants.SystemReservedClaimSets.Contains(x.ClaimSetName) - }) - .Distinct() - .OrderBy(x => x.Name) - .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) - .ToList(); - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetApplicationsByClaimSetIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetApplicationsByClaimSetIdQuery.cs index 5c24a76f8..f785cdc07 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetApplicationsByClaimSetIdQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetApplicationsByClaimSetIdQuery.cs @@ -3,8 +3,6 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System.Collections.Generic; -using System.Linq; using EdFi.Admin.DataAccess.Contexts; using EdFi.Security.DataAccess.Contexts; @@ -21,9 +19,9 @@ public GetApplicationsByClaimSetIdQuery(ISecurityContext securityContext, IUsers _usersContext = usersContext; } - public IEnumerable Execute(int claimSetId) + public IEnumerable Execute(int securityContextClaimSetId) { - var claimSetName = GetClaimSetNameById(claimSetId); + var claimSetName = GetClaimSetNameById(securityContextClaimSetId); return GetApplicationsByClaimSetName(claimSetName); } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQuery.cs index 91261bcc1..2d4e534d6 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQuery.cs @@ -3,33 +3,37 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Security.DataAccess.Contexts; + namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; public class GetClaimSetByIdQuery : IGetClaimSetByIdQuery { - private readonly IOdsSecurityModelVersionResolver _resolver; - private readonly GetClaimSetByIdQueryV53Service _v53Service; - private readonly GetClaimSetByIdQueryV6Service _v6Service; + private readonly ISecurityContext _securityContext; - public GetClaimSetByIdQuery(IOdsSecurityModelVersionResolver resolver, - GetClaimSetByIdQueryV53Service v53Service, - GetClaimSetByIdQueryV6Service v6Service) + public GetClaimSetByIdQuery(ISecurityContext securityContext) { - _resolver = resolver; - _v53Service = v53Service; - _v6Service = v6Service; + _securityContext = securityContext; } public ClaimSet Execute(int securityContextClaimSetId) { - var securityModel = _resolver.DetermineSecurityModel(); + var securityContextClaimSet = _securityContext.ClaimSets + .SingleOrDefault(x => x.ClaimSetId == securityContextClaimSetId); - return securityModel switch + if (securityContextClaimSet != null) { - EdFiOdsSecurityModelCompatibility.ThreeThroughFive or EdFiOdsSecurityModelCompatibility.FiveThreeCqe => _v53Service.Execute(securityContextClaimSetId), - EdFiOdsSecurityModelCompatibility.Six => _v6Service.Execute(securityContextClaimSetId), - _ => throw new EdFiOdsSecurityModelCompatibilityException(securityModel), - }; + return new ClaimSet + { + Id = securityContextClaimSet.ClaimSetId, + Name = securityContextClaimSet.ClaimSetName, + IsEditable = !securityContextClaimSet.ForApplicationUseOnly && !securityContextClaimSet.IsEdfiPreset + }; + } + + throw new NotFoundException("claimset", securityContextClaimSetId); + } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQueryV53Service.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQueryV53Service.cs deleted file mode 100644 index e975e0132..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetClaimSetByIdQueryV53Service.cs +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using System.Net; -using EdFi.Ods.AdminApi.Infrastructure.ErrorHandling; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; - -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; - -public class GetClaimSetByIdQueryV53Service -{ - private readonly ISecurityContext _securityContext; - - public GetClaimSetByIdQueryV53Service(ISecurityContext securityContext) - { - _securityContext = securityContext; - } - - public ClaimSet Execute(int claimSetId) - { - var securityContextClaimSet = _securityContext.ClaimSets - .SingleOrDefault(x => x.ClaimSetId == claimSetId); - - if (securityContextClaimSet != null) - { - return new ClaimSet - { - Id = securityContextClaimSet.ClaimSetId, - Name = securityContextClaimSet.ClaimSetName, - IsEditable = !Constants.DefaultClaimSets.Contains(securityContextClaimSet.ClaimSetName) - }; - } - - throw new AdminApiException("No such claim set exists in the database.") - { - StatusCode = HttpStatusCode.NotFound - }; - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQuery.cs index cb56a7c6d..0cfcf6c44 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQuery.cs @@ -3,50 +3,32 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using AutoMapper; +using EdFi.Security.DataAccess.Contexts; +using EdFi.Security.DataAccess.Models; +using SecurityResourceClaim = EdFi.Security.DataAccess.Models.ResourceClaim; +using SecurityAuthorizationStrategy = EdFi.Security.DataAccess.Models.AuthorizationStrategy; +using Microsoft.EntityFrameworkCore; + namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor { public class GetResourcesByClaimSetIdQuery : IGetResourcesByClaimSetIdQuery { - private readonly IOdsSecurityModelVersionResolver _resolver; - private readonly GetResourcesByClaimSetIdQueryV53Service _v53Service; - private readonly GetResourcesByClaimSetIdQueryV6Service _v6Service; + private readonly ISecurityContext _securityContext; + private readonly IMapper _mapper; - public GetResourcesByClaimSetIdQuery(IOdsSecurityModelVersionResolver resolver, - GetResourcesByClaimSetIdQueryV53Service v53Service, - GetResourcesByClaimSetIdQueryV6Service v6Service) + public GetResourcesByClaimSetIdQuery(ISecurityContext securityContext, IMapper mapper) { - _resolver = resolver; - _v53Service = v53Service; - _v6Service = v6Service; + _securityContext = securityContext; + _mapper = mapper; } public IList AllResources(int securityContextClaimSetId) { - IList parentResources; - var securityModel = _resolver.DetermineSecurityModel(); - - return securityModel switch { - EdFiOdsSecurityModelCompatibility.ThreeThroughFive => ModelThreeThroughFive(), - EdFiOdsSecurityModelCompatibility.FiveThreeCqe => ModelThreeThroughFive(true), - EdFiOdsSecurityModelCompatibility.Six => ModelSix(), - _ => throw new EdFiOdsSecurityModelCompatibilityException(securityModel) - }; - - IList ModelThreeThroughFive(bool IsFiveThreeCqe = false) { - parentResources = _v53Service.GetParentResources(securityContextClaimSetId, IsFiveThreeCqe); - var childResources = _v53Service.GetChildResources(securityContextClaimSetId, IsFiveThreeCqe); - _v53Service.AddChildResourcesToParents(childResources, parentResources); - - return parentResources; - } - - IList ModelSix() { - parentResources = _v6Service.GetParentResources(securityContextClaimSetId); - var childResources = _v6Service.GetChildResources(securityContextClaimSetId); - GetResourcesByClaimSetIdQueryV6Service.AddChildResourcesToParents(childResources, parentResources); - - return parentResources; - } + var parentResources = GetParentResources(securityContextClaimSetId); + var childResources = GetChildResources(securityContextClaimSetId); + AddChildResourcesToParents(childResources, parentResources); + return parentResources; } public ResourceClaim? SingleResource(int claimSetId, int resourceClaimId) @@ -63,6 +45,261 @@ IList ModelSix() { return parentResourceClaim; } + + internal static void AddChildResourcesToParents(IReadOnlyList childResources, IList parentResources) + { + foreach (var childResource in childResources) + { + var parentResource = parentResources.SingleOrDefault(x => x.Id == childResource.ParentId); + if (parentResource != null) + parentResource.Children.Add(childResource); + else + { + parentResources.Add(childResource); + } + } + } + + internal IList GetParentResources(int claimSetId) + { + var dbParentResources = _securityContext.ClaimSetResourceClaimActions + .Include(x => x.ResourceClaim) + .Include(x => x.ResourceClaim.ParentResourceClaim) + .Include(x => x.Action) + .Include(x => x.AuthorizationStrategyOverrides) + .ThenInclude(x => x.AuthorizationStrategy) + .Where(x => x.ClaimSet.ClaimSetId == claimSetId + && x.ResourceClaim.ParentResourceClaimId == null).ToList(); + + var defaultAuthStrategies = GetDefaultAuthStrategies(dbParentResources.Select(x => x.ResourceClaim).ToList()); + var authStrategyOverrides = GetAuthStrategyOverrides(dbParentResources.ToList()); + + var parentResources = dbParentResources.GroupBy(x => x.ResourceClaim).Select(x => new ResourceClaim + { + Id = x.Key.ResourceClaimId, + Name = x.Key.ResourceName, + Actions = x.Where(x => x.Action != null).Select(x => + new ResourceClaimAction { Name = x.Action.ActionName, Enabled = true}).ToList(), + IsParent = true, + DefaultAuthorizationStrategiesForCRUD = defaultAuthStrategies[x.Key.ResourceClaimId], + AuthorizationStrategyOverridesForCRUD = authStrategyOverrides[x.Key.ResourceClaimId] + }).ToList(); + + parentResources.ForEach(x => x.Children = new List()); + return parentResources; + } + + public Dictionary> GetDefaultAuthStrategies(IReadOnlyCollection resourceClaims) + { + var resultDictionary = new Dictionary>(); + + var defaultAuthStrategies = _securityContext.ResourceClaimActions + .Include(x => x.ResourceClaim) + .Include(x => x.Action) + .Include(x => x.AuthorizationStrategies) + .ThenInclude(x => x.AuthorizationStrategy).ToList(); + + var defaultAuthStrategiesForParents = defaultAuthStrategies + .Where(x => x.ResourceClaim.ParentResourceClaimId == null).ToList(); + + var defaultAuthStrategiesForChildren = defaultAuthStrategies + .Where(x => x.ResourceClaim.ParentResourceClaimId != null).ToList(); + + foreach (var resourceClaim in resourceClaims) + { + var actions = new List(); + foreach (var action in _securityContext.Actions) + { + if (resourceClaim.ParentResourceClaimId == null) + { + var defaultStrategies = defaultAuthStrategiesForParents.SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && + x.Action.ActionName == action.ActionName)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + + if (defaultStrategies != null) + { + actions.Add(new ClaimSetResourceClaimActionAuthStrategies + { + ActionId = action.ActionId, + ActionName = action.ActionName, + AuthorizationStrategies = _mapper.Map>(defaultStrategies) + }); + } + } + else + { + List? childResourceStrategies = null; + var defaultStrategies = defaultAuthStrategiesForChildren.SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && + x.Action.ActionName == action.ActionName)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + + if (defaultStrategies == null) + { + defaultStrategies = defaultAuthStrategiesForParents.SingleOrDefault(x => + x.ResourceClaim.ResourceClaimId == resourceClaim.ParentResourceClaimId && + x.Action.ActionName == action.ActionName)?.AuthorizationStrategies?.Select(x => x.AuthorizationStrategy); + childResourceStrategies = AddStrategiesToChildResource(defaultStrategies, true); + } + else + { + childResourceStrategies = AddStrategiesToChildResource(defaultStrategies); + } + if (childResourceStrategies != null) + { + actions.Add(new ClaimSetResourceClaimActionAuthStrategies + { + ActionId = action.ActionId, + ActionName = action.ActionName, + AuthorizationStrategies = _mapper.Map>(childResourceStrategies) + }); + } + } + } + + static List AddStrategiesToChildResource(IEnumerable? authStrategies, bool fromParent = false) + { + var strategies = new List(); + if (authStrategies != null && authStrategies.Any()) + { + foreach (var authStratregy in authStrategies) + { + strategies.Add(new AuthorizationStrategy + { + AuthStrategyId = authStratregy.AuthorizationStrategyId, + AuthStrategyName = authStratregy.AuthorizationStrategyName, + IsInheritedFromParent = fromParent + }); + } + } + return strategies; + } + + resultDictionary[resourceClaim.ResourceClaimId] = actions.Where(x => x != null && + x.AuthorizationStrategies != null && x.AuthorizationStrategies.Any()).ToList(); + } + + return resultDictionary; + } + + private Dictionary> GetAuthStrategyOverrides(List resourceClaims) + { + var resultDictionary = new Dictionary>(); + + foreach (var resourceClaim in resourceClaims) + { + ClaimSetResourceClaimActionAuthStrategies? actionDetails = null; + + if (resourceClaim.ResourceClaim.ParentResourceClaim == null) + { + if (resourceClaim.AuthorizationStrategyOverrides is not null + && resourceClaim.AuthorizationStrategyOverrides.Count != 0) + { + actionDetails = new ClaimSetResourceClaimActionAuthStrategies + { + ActionId = resourceClaim.ActionId, + ActionName = resourceClaim.Action.ActionName, + AuthorizationStrategies = AddStrategyOverridesToResource(resourceClaim.AuthorizationStrategyOverrides) + }; + } + } + else + { + var parentResources = _securityContext.ClaimSetResourceClaimActions + .Include(x => x.ResourceClaim) + .Include(x => x.ClaimSet) + .Include(x => x.Action) + .Include(x => x.AuthorizationStrategyOverrides) + .ThenInclude(x => x.AuthorizationStrategy).ToList(); + + var parentResourceOverride = parentResources.SingleOrDefault(x => x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaim.ParentResourceClaimId + && x.ClaimSet.ClaimSetId == resourceClaim.ClaimSet.ClaimSetId + && x.Action.ActionId == resourceClaim.Action.ActionId); + + List? childResourceOverrideStrategies = null; + + if (parentResourceOverride?.AuthorizationStrategyOverrides != null && parentResourceOverride.AuthorizationStrategyOverrides.Count != 0) + { + childResourceOverrideStrategies = AddStrategyOverridesToResource(parentResourceOverride.AuthorizationStrategyOverrides, true); + } + + if (resourceClaim.AuthorizationStrategyOverrides != null && resourceClaim.AuthorizationStrategyOverrides.Count != 0) + { + childResourceOverrideStrategies = AddStrategyOverridesToResource(resourceClaim.AuthorizationStrategyOverrides); + } + + if (childResourceOverrideStrategies != null) + { + actionDetails = new ClaimSetResourceClaimActionAuthStrategies + { + ActionId = resourceClaim.ActionId, + ActionName = resourceClaim.Action.ActionName, + AuthorizationStrategies = childResourceOverrideStrategies + }; + } + } + + static List AddStrategyOverridesToResource(IEnumerable? authStrategies, bool fromParent = false) + { + var strategies = new List(); + if (authStrategies != null && authStrategies.Any()) + { + foreach (var authStrategy in authStrategies) + { + strategies.Add(new AuthorizationStrategy + { + AuthStrategyId = authStrategy.AuthorizationStrategyId, + AuthStrategyName = authStrategy.AuthorizationStrategy?.AuthorizationStrategyName, + IsInheritedFromParent = fromParent + }); + } + } + return strategies; + } + + if (resultDictionary.ContainsKey(resourceClaim.ResourceClaim.ResourceClaimId)) + { + if (actionDetails != null) + resultDictionary[resourceClaim.ResourceClaim.ResourceClaimId].Add(actionDetails); + } + else + { + resultDictionary[resourceClaim.ResourceClaim.ResourceClaimId] = []; + if(actionDetails != null) + { + resultDictionary[resourceClaim.ResourceClaim.ResourceClaimId].Add(actionDetails); + } + } + } + + return resultDictionary; + } + + internal IReadOnlyList GetChildResources(int claimSetId) + { + var dbChildResources = + _securityContext.ClaimSetResourceClaimActions + .Include(x => x.ResourceClaim) + .Include(x => x.Action) + .Where(x => x.ClaimSet.ClaimSetId == claimSetId + && x.ResourceClaim.ParentResourceClaimId != null).ToList(); + var defaultAuthStrategies = GetDefaultAuthStrategies(dbChildResources.Select(x => x.ResourceClaim).ToList()); + var authStrategyOverrides = GetAuthStrategyOverrides([.. dbChildResources]); + + var childResources = dbChildResources.GroupBy(x => x.ResourceClaim) + .Select(x => new ResourceClaim + { + Id = x.Key.ResourceClaimId, + ParentId = x.Key.ParentResourceClaimId ?? 0, + Name = x.Key.ResourceName, + Actions = x.Where(x => x.Action != null).Select(x => + new ResourceClaimAction { Name = x.Action.ActionName, Enabled = true }).ToList(), + IsParent = false, + DefaultAuthorizationStrategiesForCRUD = defaultAuthStrategies[x.Key.ResourceClaimId], + AuthorizationStrategyOverridesForCRUD = authStrategyOverrides[x.Key.ResourceClaimId] + }) + .ToList(); + return childResources; + } } public interface IGetResourcesByClaimSetIdQuery diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQueryV53Service.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQueryV53Service.cs deleted file mode 100644 index 93d951bdc..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/GetResourcesByClaimSetIdQueryV53Service.cs +++ /dev/null @@ -1,281 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using AutoMapper; -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor.Extensions; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models; -using SecurityAuthorizationStrategy = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.AuthorizationStrategy; -using SecurityResourceClaim = Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models.ResourceClaim; -using Microsoft.EntityFrameworkCore; - -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor -{ - public class GetResourcesByClaimSetIdQueryV53Service - { - private readonly ISecurityContext _securityContext; - private readonly IMapper _mapper; - - public GetResourcesByClaimSetIdQueryV53Service(ISecurityContext securityContext, IMapper mapper) - { - _securityContext = securityContext; - _mapper = mapper; - } - - internal void AddChildResourcesToParents(IReadOnlyList childResources, IList parentResources) - { - foreach (var childResource in childResources) - { - var parentResource = parentResources.SingleOrDefault(x => x.Id == childResource.ParentId); - if (parentResource != null) - parentResource.Children.Add(childResource); - else - { - parentResources.Add(childResource); - } - } - } - - internal IList GetParentResources(int claimSetId, bool IsFiveThreeCqe) - { - var dbParentResources = _securityContext.ClaimSetResourceClaims - .Include(x => x.ResourceClaim) - .Include(x => x.ResourceClaim.ParentResourceClaim) - .Include(x => x.Action) - .Include(x => x.AuthorizationStrategyOverride) - .Where(x => x.ClaimSet.ClaimSetId == claimSetId - && x.ResourceClaim.ParentResourceClaimId == null).ToList(); - - var defaultAuthStrategies = GetDefaultAuthStrategies(dbParentResources.Select(x => x.ResourceClaim).ToList(), IsFiveThreeCqe); - var authStrategyOverrides = GetAuthStrategyOverrides(dbParentResources.ToList(), IsFiveThreeCqe); - - var parentResources = dbParentResources.GroupBy(x => x.ResourceClaim).Select(x => new ResourceClaim - { - Id = x.Key.ResourceClaimId, - Name = x.Key.ResourceName, - Create = x.Any(a => a.Action.ActionName == Action.Create.Value), - Read = x.Any(a => a.Action.ActionName == Action.Read.Value), - Update = x.Any(a => a.Action.ActionName == Action.Update.Value), - Delete = x.Any(a => a.Action.ActionName == Action.Delete.Value), - ReadChanges = x.Any(a => a.Action.ActionName == Action.ReadChanges.Value), - IsParent = true, - DefaultAuthStrategiesForCRUD = defaultAuthStrategies[x.Key.ResourceClaimId], - AuthStrategyOverridesForCRUD = authStrategyOverrides[x.Key.ResourceClaimId].ToArray() - }).ToList(); - - parentResources.ForEach(x => x.Children = new List()); - return parentResources; - } - - private Dictionary GetDefaultAuthStrategies(IReadOnlyCollection resourceClaims, bool IsFiveThreeCqe) - { - var resultDictionary = new Dictionary(); - - var defaultAuthStrategies = _securityContext.ResourceClaimAuthorizationMetadatas - .Include(x => x.ResourceClaim).Include(x => x.Action).Include(x => x.AuthorizationStrategy).ToList(); - - var defaultAuthStrategiesForParents = defaultAuthStrategies - .Where(x => x.ResourceClaim.ParentResourceClaimId == null).ToList(); - - var defaultAuthStrategiesForChildren = defaultAuthStrategies - .Where(x => x.ResourceClaim.ParentResourceClaimId != null).ToList(); - - foreach (var resourceClaim in resourceClaims) - { - var actions = new List(); - if (resourceClaim.ParentResourceClaimId == null) - { - var createDefaultStrategy = defaultAuthStrategiesForParents.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Create.Value)?.AuthorizationStrategy; - AddStrategyToParentResource(createDefaultStrategy); - var readDefaultStrategy = defaultAuthStrategiesForParents.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Read.Value)?.AuthorizationStrategy; - AddStrategyToParentResource(readDefaultStrategy); - var updateDefaultStrategy = defaultAuthStrategiesForParents - .SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Update.Value)?.AuthorizationStrategy; - AddStrategyToParentResource(updateDefaultStrategy); - var deleteDefaultStrategy = defaultAuthStrategiesForParents - .SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Delete.Value)?.AuthorizationStrategy; - AddStrategyToParentResource(deleteDefaultStrategy); - - if (IsFiveThreeCqe) - { - var readChangesDefaultStrategy = defaultAuthStrategiesForParents - .SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.ReadChanges.Value)?.AuthorizationStrategy; - AddStrategyToParentResource(readChangesDefaultStrategy); - } - - void AddStrategyToParentResource(SecurityAuthorizationStrategy? defaultStrategy) - { - actions.Add(defaultStrategy != null ? new ClaimSetResourceClaimActionAuthStrategies() - { - AuthorizationStrategies = new List - { _mapper.Map(defaultStrategy) } - } : null); - } - } - else - { - var createDefaultStrategy = defaultAuthStrategiesForChildren.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Create.Value)?.AuthorizationStrategy; - actions = AddStrategyToChildResource(createDefaultStrategy, Action.Create); - - var readDefaultStrategy = defaultAuthStrategiesForChildren.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Read.Value)?.AuthorizationStrategy; - actions = AddStrategyToChildResource(readDefaultStrategy, Action.Read); - - var updateDefaultStrategy = defaultAuthStrategiesForChildren.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Update.Value)?.AuthorizationStrategy; - actions = AddStrategyToChildResource(updateDefaultStrategy, Action.Update); - - var deleteDefaultStrategy = defaultAuthStrategiesForChildren.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.Delete.Value)?.AuthorizationStrategy; - actions = AddStrategyToChildResource(deleteDefaultStrategy, Action.Delete); - - if (IsFiveThreeCqe) - { - var readChangesDefaultStrategy = defaultAuthStrategiesForChildren.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaimId && - x.Action.ActionName == Action.ReadChanges.Value)?.AuthorizationStrategy; - actions = AddStrategyToChildResource(readChangesDefaultStrategy, Action.ReadChanges); - } - - List AddStrategyToChildResource(SecurityAuthorizationStrategy? defaultStrategy, Action action) - { - if (defaultStrategy == null) - { - defaultStrategy = defaultAuthStrategiesForParents.SingleOrDefault(x => - x.ResourceClaim.ResourceClaimId == resourceClaim.ParentResourceClaimId && - x.Action.ActionName == action.Value)?.AuthorizationStrategy; - var mappedStrategy = _mapper.Map(defaultStrategy); - if (mappedStrategy != null) mappedStrategy.IsInheritedFromParent = true; - - var strategy = mappedStrategy != null ? new ClaimSetResourceClaimActionAuthStrategies() - { - AuthorizationStrategies = new List { _mapper.Map(mappedStrategy) }.ToArray() - } : null; - - actions.Add(strategy); - } - else - { - actions.Add(new ClaimSetResourceClaimActionAuthStrategies() - { - AuthorizationStrategies = new List { _mapper.Map(defaultStrategy) }.ToArray() - }); - } - - return actions; - } - } - - resultDictionary[resourceClaim.ResourceClaimId] = actions.ToArray() as ClaimSetResourceClaimActionAuthStrategies[]; - } - - return resultDictionary; - } - - - internal Dictionary GetAuthStrategyOverrides(List resourceClaims, bool IsFiveThreeCqe) - { - var resultDictionary = new Dictionary(); - resourceClaims = - new List(resourceClaims.OrderBy(i => new List { Action.Create.Value, Action.Read.Value, Action.Update.Value, Action.Delete.Value, Action.ReadChanges.Value }.IndexOf(i.Action.ActionName))); - foreach (var resourceClaim in resourceClaims) - { - AuthorizationStrategy? authStrategy = null; - if (resourceClaim.ResourceClaim.ParentResourceClaim == null) - { - authStrategy = _mapper.Map(resourceClaim.AuthorizationStrategyOverride); - } - else - { - var parentResources = _securityContext.ClaimSetResourceClaims - .Include(x => x.ResourceClaim) - .Include(x => x.ClaimSet) - .Include(x => x.Action) - .Include(x => x.AuthorizationStrategyOverride).ToList(); - var parentResourceOverride = parentResources.SingleOrDefault(x => x.ResourceClaim.ResourceClaimId == resourceClaim.ResourceClaim.ParentResourceClaimId - && x.ClaimSet.ClaimSetId == resourceClaim.ClaimSet.ClaimSetId - && x.Action.ActionId == resourceClaim.Action.ActionId); - if (parentResourceOverride?.AuthorizationStrategyOverride != null) - { - authStrategy = - _mapper.Map(parentResourceOverride.AuthorizationStrategyOverride); - if (authStrategy != null) - { - authStrategy.IsInheritedFromParent = true; - } - } - - if (resourceClaim.AuthorizationStrategyOverride != null) - { - authStrategy = _mapper.Map(resourceClaim.AuthorizationStrategyOverride); - } - } - - if (resultDictionary.ContainsKey(resourceClaim.ResourceClaim.ResourceClaimId)) - { - resultDictionary[resourceClaim.ResourceClaim.ResourceClaimId].AddAuthorizationStrategyOverrides(resourceClaim.Action.ActionName, authStrategy); - } - else - { - var arrayLength = 4; - if (IsFiveThreeCqe) - { - arrayLength = 5; - } - resultDictionary[resourceClaim.ResourceClaim.ResourceClaimId] = new ClaimSetResourceClaimActionAuthStrategies[arrayLength]; - resultDictionary[resourceClaim.ResourceClaim.ResourceClaimId].AddAuthorizationStrategyOverrides(resourceClaim.Action.ActionName, authStrategy); - } - } - return resultDictionary; - } - - internal IReadOnlyList GetChildResources(int claimSetId, bool IsFiveThreeCqe) - { - var dbChildResources = - _securityContext.ClaimSetResourceClaims - .Include(x => x.ResourceClaim) - .Include(x => x.Action) - .Include(x => x.AuthorizationStrategyOverride) - .Where(x => x.ClaimSet.ClaimSetId == claimSetId - && x.ResourceClaim.ParentResourceClaimId != null).ToList(); - var defaultAuthStrategies = GetDefaultAuthStrategies(dbChildResources.Select(x => x.ResourceClaim).ToList(), IsFiveThreeCqe); - var authStrategyOverrides = GetAuthStrategyOverrides(dbChildResources.ToList(), IsFiveThreeCqe); - - var childResources = dbChildResources.GroupBy(x => x.ResourceClaim) - .Select(x => new ResourceClaim - { - Id = x.Key.ResourceClaimId, - ParentId = x.Key.ParentResourceClaimId ?? 0, - Name = x.Key.ResourceName, - Create = x.Any(a => a.Action.ActionName == Action.Create.Value), - Read = x.Any(a => a.Action.ActionName == Action.Read.Value), - Update = x.Any(a => a.Action.ActionName == Action.Update.Value), - Delete = x.Any(a => a.Action.ActionName == Action.Delete.Value), - ReadChanges = x.Any(a => a.Action.ActionName == Action.ReadChanges.Value), - IsParent = false, - DefaultAuthStrategiesForCRUD = defaultAuthStrategies[x.Key.ResourceClaimId], - AuthStrategyOverridesForCRUD = authStrategyOverrides.Keys.Any(p => p == x.Key.ResourceClaimId) ? authStrategyOverrides[x.Key.ResourceClaimId] : Array.Empty(), - }).ToList(); - return childResources; - } - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyCommand.cs index d08f66be7..35af93de2 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyCommand.cs @@ -3,46 +3,262 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.AdminApi.Infrastructure.Documentation; +using EdFi.Security.DataAccess.Contexts; +using EdFi.Security.DataAccess.Models; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.EntityFrameworkCore; + namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; public class OverrideDefaultAuthorizationStrategyCommand { - private readonly IOdsSecurityModelVersionResolver _resolver; - private readonly OverrideDefaultAuthorizationStrategyV53Service _v53Service; - private readonly OverrideDefaultAuthorizationStrategyV6Service _v6Service; - - public OverrideDefaultAuthorizationStrategyCommand(IOdsSecurityModelVersionResolver resolver, OverrideDefaultAuthorizationStrategyV53Service v53Service, OverrideDefaultAuthorizationStrategyV6Service v6Service) + private readonly ISecurityContext _context; + public OverrideDefaultAuthorizationStrategyCommand(ISecurityContext context) { - _resolver = resolver; - _v53Service = v53Service; - _v6Service = v6Service; + _context = context; } public void Execute(IOverrideDefaultAuthorizationStrategyModel model) { - var securityModel = _resolver.DetermineSecurityModel(); - switch (securityModel) + var claimSetResourceClaimsToEdit = GetClaimSetResourceClaimsToEdit(model.ClaimSetId, model.ResourceClaimId); + var parentResourceClaims = GetParentResourceClaims(model.ClaimSetId, model.ResourceClaimId); + + var authorizationStrategiesDictionary = GetAuthorizationStrategiesAsDictionary(); + + claimSetResourceClaimsToEdit = RemoveOverrides(model, claimSetResourceClaimsToEdit); + + AddOverrides(model, claimSetResourceClaimsToEdit, authorizationStrategiesDictionary, parentResourceClaims); + + _context.SaveChanges(); + } + + + public void ExecuteOnSpecificAction(OverrideAuthStrategyOnClaimSetModel model) + { + var authorizationStrategiesDictionary = GetAuthorizationStrategiesAsDictionary(); + var claimSetResourceClaimsToEdit = GetClaimSetResourceClaimsToEdit(model.ClaimSetId, model.ResourceClaimId); + + var claimSetResourceClaimAction = claimSetResourceClaimsToEdit.Find(rc => rc.ResourceClaimId == model.ResourceClaimId && + rc.Action.ActionName.Equals(model.ActionName, StringComparison.CurrentCultureIgnoreCase)); + + if (claimSetResourceClaimAction != null) + { + var resourceClaimActionDefaultAuthorizationStrategy = _context.ResourceClaimActionAuthorizationStrategies.AsEnumerable().FirstOrDefault(p => p.ResourceClaimAction.ResourceClaimId == model.ResourceClaimId + && p.ResourceClaimAction.Action.ActionName.Equals(model.ActionName, StringComparison.CurrentCultureIgnoreCase)); + + if (resourceClaimActionDefaultAuthorizationStrategy == null && claimSetResourceClaimAction.ResourceClaim.ParentResourceClaim != null) + { + var parentResourceClaimId = claimSetResourceClaimAction.ResourceClaim.ParentResourceClaim.ResourceClaimId; + var parentResourceClaimDefaultAuthStrategy = _context.ResourceClaimActionAuthorizationStrategies.FirstOrDefault(p => + p.ResourceClaimAction.ResourceClaimId == parentResourceClaimId && p.ResourceClaimAction.Action.ActionName.ToLower() == model.ActionName!.ToLower()); + resourceClaimActionDefaultAuthorizationStrategy = parentResourceClaimDefaultAuthStrategy; + } + + if (!claimSetResourceClaimAction!.AuthorizationStrategyOverrides.Exists(rc => rc.ClaimSetResourceClaimAction.Action.ActionName.Equals(model.ActionName, StringComparison.CurrentCultureIgnoreCase))) + { + if (model.AuthStrategyIds != null) + foreach (var id in model.AuthStrategyIds) + { + if (resourceClaimActionDefaultAuthorizationStrategy != null && + resourceClaimActionDefaultAuthorizationStrategy.AuthorizationStrategyId != id) + { + claimSetResourceClaimAction!.AuthorizationStrategyOverrides.Add(new ClaimSetResourceClaimActionAuthorizationStrategyOverrides() + { + AuthorizationStrategy = authorizationStrategiesDictionary[id] + }); + } + + } + } + else + { + if (model.AuthStrategyIds != null) + { + RemoveClaimSetResourceClaimActionAuthorizationStrategyOverrides(claimSetResourceClaimAction); + + var overrideAuthStrategies = new List(); + foreach (var id in model.AuthStrategyIds) + { + if (resourceClaimActionDefaultAuthorizationStrategy != null && + resourceClaimActionDefaultAuthorizationStrategy.AuthorizationStrategyId != id) + { + overrideAuthStrategies.Add(new ClaimSetResourceClaimActionAuthorizationStrategyOverrides() + { + AuthorizationStrategy = authorizationStrategiesDictionary[id] + }); + } + } + claimSetResourceClaimAction.AuthorizationStrategyOverrides = overrideAuthStrategies; + } + } + _context.SaveChanges(); + } + else + { + var validationErrors = new List + { + new() { + PropertyName = "Action", + ErrorMessage = $"{model.ActionName} action is not enabled for the resource claim with id {model.ResourceClaimId}." + } + }; + throw new ValidationException(validationErrors); + } + } + + public void ResetAuthorizationStrategyOverrides(OverrideAuthStrategyOnClaimSetModel model) + { + var claimSetResourceClaimActionsToEdit = GetClaimSetResourceClaimsToEdit(model.ClaimSetId, model.ResourceClaimId); + + foreach (var resourceClaimAction in claimSetResourceClaimActionsToEdit) + { + RemoveClaimSetResourceClaimActionAuthorizationStrategyOverrides(resourceClaimAction); + } + _context.SaveChanges(); + } + + private Dictionary GetAuthorizationStrategiesAsDictionary() + { + var authorizationStrategiesDictionary = + new Dictionary(); + + foreach (var authStrategy in _context.AuthorizationStrategies.ToList()) + { + authorizationStrategiesDictionary[authStrategy.AuthorizationStrategyId] = authStrategy; + } + + return authorizationStrategiesDictionary; + } + + private List GetParentResourceClaims(int claimSetId, int resourceClaimId) + { + var parentResourceClaims = new List(); + var parentResourceClaimId = _context.ResourceClaims + .Single(x => x.ResourceClaimId == resourceClaimId).ParentResourceClaimId; + if (parentResourceClaimId != null) + { + parentResourceClaims = [.. _context.ClaimSetResourceClaimActions + .Include(x => x.ResourceClaim) + .Include(x => x.Action) + .Include(x => x.ClaimSet) + .Include(x => x.AuthorizationStrategyOverrides) + .ThenInclude(x => x.AuthorizationStrategy) + .Where( + x => x.ResourceClaim.ResourceClaimId == parentResourceClaimId && + x.ClaimSet.ClaimSetId == claimSetId)]; + } + + return parentResourceClaims; + } + + private List GetClaimSetResourceClaimsToEdit(int claimSetId, int resourceClaimId) + { + return [.. _context.ClaimSetResourceClaimActions + .Include(x => x.ResourceClaim) + .Include(x => x.Action) + .Include(x => x.ClaimSet) + .Include(x => x.AuthorizationStrategyOverrides) + .ThenInclude(x => x.AuthorizationStrategy) + .Where( + x => x.ResourceClaim.ResourceClaimId == resourceClaimId && + x.ClaimSet.ClaimSetId == claimSetId)]; + } + + private List RemoveOverrides( + IOverrideDefaultAuthorizationStrategyModel model, + IEnumerable resourceClaimsToEdit) + { + var claimSetResourceClaims = resourceClaimsToEdit.ToList(); + + foreach (var claimSetResourceClaim in claimSetResourceClaims) + { + var actionAuthStrategiesToOverride = model.ClaimSetResourceClaimActionAuthStrategyOverrides?.Where(x => + x != null && x.ActionName != null && x.ActionName.Equals(claimSetResourceClaim.Action.ActionName, StringComparison.InvariantCultureIgnoreCase)); + + if (actionAuthStrategiesToOverride != null && actionAuthStrategiesToOverride.Any()) + { + claimSetResourceClaim.AuthorizationStrategyOverrides = null; + } + + RemoveClaimSetResourceClaimActionAuthorizationStrategyOverrides(claimSetResourceClaim); + } + + return claimSetResourceClaims; + } + + private void RemoveClaimSetResourceClaimActionAuthorizationStrategyOverrides(ClaimSetResourceClaimAction claimSetResourceClaimAction) + { + var existingAuthOverrides = + _context.ClaimSetResourceClaimActionAuthorizationStrategyOverrides.Where( + x => x.ClaimSetResourceClaimActionId == + claimSetResourceClaimAction.ClaimSetResourceClaimActionId); + + if (existingAuthOverrides.Any()) { - case EdFiOdsSecurityModelCompatibility.ThreeThroughFive or EdFiOdsSecurityModelCompatibility.FiveThreeCqe: - _v53Service.Execute(model); - break; - case EdFiOdsSecurityModelCompatibility.Six: - _v6Service.Execute(model); - break; - default: - throw new EdFiOdsSecurityModelCompatibilityException(securityModel); + _context.ClaimSetResourceClaimActionAuthorizationStrategyOverrides.RemoveRange( + existingAuthOverrides); + } + } + + private static void AddOverrides(IOverrideDefaultAuthorizationStrategyModel model, + IEnumerable resourceClaimsToEdit, + Dictionary authorizationStrategiesDictionary, + List parentResourceClaims) + { + var claimSetResourceClaims = resourceClaimsToEdit.ToList(); + + foreach (var claimSetResourceClaim in claimSetResourceClaims) + { + var actionAuthStrategiesToOverride = model.ClaimSetResourceClaimActionAuthStrategyOverrides?.Find(x => + x != null && x.ActionName != null && x.ActionName.Equals(claimSetResourceClaim.Action.ActionName, StringComparison.InvariantCultureIgnoreCase)); + + List? authStrategyOverrides = null; + + if (actionAuthStrategiesToOverride != null) + { + authStrategyOverrides = []; + if (actionAuthStrategiesToOverride.AuthorizationStrategies != null) + foreach (var actionAuthOverride in actionAuthStrategiesToOverride.AuthorizationStrategies) + { + authStrategyOverrides.Add(new() + { + AuthorizationStrategy = authorizationStrategiesDictionary[actionAuthOverride.AuthStrategyId] + }); + } + + if (parentResourceClaims.Count != 0 && parentResourceClaims.SingleOrDefault( + x => x.Action.ActionId == claimSetResourceClaim.ActionId + && x.AuthorizationStrategyOverrides != null && + x.AuthorizationStrategyOverrides.Exists(a => + authStrategyOverrides.Select(c => c.AuthorizationStrategyId). + Contains(a.AuthorizationStrategyId))) == null + || parentResourceClaims.Count == 0) + { + claimSetResourceClaim.AuthorizationStrategyOverrides = authStrategyOverrides; + } + } } } } + public interface IOverrideDefaultAuthorizationStrategyModel { int ClaimSetId { get; } int ResourceClaimId { get; } - int[]? AuthorizationStrategyForCreate { get; } - int[]? AuthorizationStrategyForRead { get; } - int[]? AuthorizationStrategyForUpdate { get; } - int[]? AuthorizationStrategyForDelete { get; } - int[]? AuthorizationStrategyForReadChanges { get; } + List? ClaimSetResourceClaimActionAuthStrategyOverrides { get; } +} + +public class OverrideAuthStrategyOnClaimSetModel +{ + [SwaggerExclude] + public int ClaimSetId { get; set; } + [SwaggerExclude] + public int ResourceClaimId { get; set; } + public string? ActionName { get; set; } + [SwaggerExclude] + public List? AuthStrategyIds { get; set; } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyV53Service.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyV53Service.cs deleted file mode 100644 index 00d65d161..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/OverrideDefaultAuthorizationStrategyV53Service.cs +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Models; -using Microsoft.EntityFrameworkCore; - -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; - -public class OverrideDefaultAuthorizationStrategyV53Service -{ - private readonly ISecurityContext _context; - - public OverrideDefaultAuthorizationStrategyV53Service(ISecurityContext context) - { - _context = context; - } - - public void Execute(IOverrideDefaultAuthorizationStrategyModel model) - { - var claimSetResourceClaimsToEdit = _context.ClaimSetResourceClaims - .Include(x => x.ResourceClaim) - .Include(x => x.Action) - .Include(x => x.ClaimSet) - .Include(x => x.AuthorizationStrategyOverride) - .Where(x => x.ResourceClaim.ResourceClaimId == model.ResourceClaimId && x.ClaimSet.ClaimSetId == model.ClaimSetId) - .ToList(); - - var parentResourceClaimId = _context.ResourceClaims - .Single(x => x.ResourceClaimId == model.ResourceClaimId).ParentResourceClaimId; - var parentResourceClaims = new List(); - - if (parentResourceClaimId != null) - { - parentResourceClaims = _context.ClaimSetResourceClaims - .Include(x => x.ResourceClaim) - .Include(x => x.Action) - .Include(x => x.ClaimSet) - .Include(x => x.AuthorizationStrategyOverride) - .Where(x => x.ResourceClaim.ResourceClaimId == parentResourceClaimId && x.ClaimSet.ClaimSetId == model.ClaimSetId) - .ToList(); - } - - var authorizationStrategiesDictionary = new Dictionary(); - foreach (var authStrategy in _context.AuthorizationStrategies.ToList()) - { - authorizationStrategiesDictionary[authStrategy.AuthorizationStrategyId] = authStrategy; - } - - claimSetResourceClaimsToEdit = RemoveOverrides(model, claimSetResourceClaimsToEdit); - - AddOverrides(model, claimSetResourceClaimsToEdit, authorizationStrategiesDictionary, parentResourceClaims); - - _context.SaveChanges(); - } - - private static List RemoveOverrides(IOverrideDefaultAuthorizationStrategyModel model, IEnumerable resourceClaimsToEdit) - { - var claimSetResourceClaims = resourceClaimsToEdit.ToList(); - foreach (var claimSetResourceClaim in claimSetResourceClaims) - { - if (claimSetResourceClaim.Action.ActionName == Action.Create.Value && model.AuthorizationStrategyForCreate != null && model.AuthorizationStrategyForCreate.Length == 0) - { - claimSetResourceClaim.AuthorizationStrategyOverride = null; - } - else if (claimSetResourceClaim.Action.ActionName == Action.Read.Value && model.AuthorizationStrategyForRead != null && model.AuthorizationStrategyForRead.Length == 0) - { - claimSetResourceClaim.AuthorizationStrategyOverride = null; - } - else if (claimSetResourceClaim.Action.ActionName == Action.Update.Value && model.AuthorizationStrategyForUpdate != null && model.AuthorizationStrategyForUpdate.Length == 0) - { - claimSetResourceClaim.AuthorizationStrategyOverride = null; - } - else if (claimSetResourceClaim.Action.ActionName == Action.Delete.Value && model.AuthorizationStrategyForDelete != null && model.AuthorizationStrategyForDelete.Length == 0) - { - claimSetResourceClaim.AuthorizationStrategyOverride = null; - } - else if (claimSetResourceClaim.Action.ActionName == Action.ReadChanges.Value && model.AuthorizationStrategyForReadChanges != null - && model.AuthorizationStrategyForReadChanges.Length == 0) - { - claimSetResourceClaim.AuthorizationStrategyOverride = null; - } - } - - return claimSetResourceClaims; - } - - private static void AddOverrides(IOverrideDefaultAuthorizationStrategyModel model, - IEnumerable resourceClaimsToEdit, - Dictionary authorizationStrategiesDictionary, - List parentResourceClaims) - { - var claimSetResourceClaims = resourceClaimsToEdit.ToList(); - foreach (var claimSetResourceClaim in claimSetResourceClaims) - { - - if (claimSetResourceClaim.Action.ActionName == Action.Create.Value && model.AuthorizationStrategyForCreate != null && model.AuthorizationStrategyForCreate.Length != 0) - { - foreach (var authStrategyId in model.AuthorizationStrategyForCreate) - { - if (authStrategyId != 0) - { - if (parentResourceClaims.Any() && parentResourceClaims.SingleOrDefault(x => - x.Action.ActionName == Action.Create.Value && x.AuthorizationStrategyOverride != null && - x.AuthorizationStrategyOverride.AuthorizationStrategyId == authStrategyId) == null) - { - claimSetResourceClaim.AuthorizationStrategyOverride = authorizationStrategiesDictionary[authStrategyId]; - } - else if (!parentResourceClaims.Any()) - { - claimSetResourceClaim.AuthorizationStrategyOverride = authorizationStrategiesDictionary[authStrategyId]; - } - } - } - } - else if (claimSetResourceClaim.Action.ActionName == Action.Read.Value && model.AuthorizationStrategyForRead != null && model.AuthorizationStrategyForRead.Length != 0) - { - foreach (var authStrategyId in model.AuthorizationStrategyForRead) - { - if (authStrategyId != 0) - { - if (parentResourceClaims.Any() && parentResourceClaims.SingleOrDefault(x => - x.Action.ActionName == Action.Read.Value && x.AuthorizationStrategyOverride != null && - x.AuthorizationStrategyOverride.AuthorizationStrategyId == authStrategyId) == null) - { - claimSetResourceClaim.AuthorizationStrategyOverride = authorizationStrategiesDictionary[authStrategyId]; - } - else if (!parentResourceClaims.Any()) - { - claimSetResourceClaim.AuthorizationStrategyOverride = authorizationStrategiesDictionary[authStrategyId]; - } - } - } - } - else if (claimSetResourceClaim.Action.ActionName == Action.Update.Value && model.AuthorizationStrategyForUpdate != null && model.AuthorizationStrategyForUpdate.Length != 0) - { - foreach (var authStrategyId in model.AuthorizationStrategyForUpdate) - { - if (authStrategyId != 0) - { - if (parentResourceClaims.Any() && parentResourceClaims.SingleOrDefault(x => - x.Action.ActionName == Action.Update.Value && x.AuthorizationStrategyOverride != null && - x.AuthorizationStrategyOverride.AuthorizationStrategyId == authStrategyId) == null) - { - claimSetResourceClaim.AuthorizationStrategyOverride = authorizationStrategiesDictionary[authStrategyId]; - } - else if (!parentResourceClaims.Any()) - { - claimSetResourceClaim.AuthorizationStrategyOverride = authorizationStrategiesDictionary[authStrategyId]; - } - } - } - - } - else if (claimSetResourceClaim.Action.ActionName == Action.Delete.Value && model.AuthorizationStrategyForDelete != null && model.AuthorizationStrategyForDelete.Length != 0) - { - foreach (var authStrategyId in model.AuthorizationStrategyForDelete) - { - if (authStrategyId != 0) - { - if (parentResourceClaims.Any() && parentResourceClaims.SingleOrDefault(x => - x.Action.ActionName == Action.Delete.Value && x.AuthorizationStrategyOverride != null && - x.AuthorizationStrategyOverride.AuthorizationStrategyId == authStrategyId) == null) - { - claimSetResourceClaim.AuthorizationStrategyOverride = authorizationStrategiesDictionary[authStrategyId]; - } - else if (!parentResourceClaims.Any()) - { - claimSetResourceClaim.AuthorizationStrategyOverride = authorizationStrategiesDictionary[authStrategyId]; - } - } - } - - } - else if (claimSetResourceClaim.Action.ActionName == Action.ReadChanges.Value && model.AuthorizationStrategyForReadChanges != null - && model.AuthorizationStrategyForReadChanges.Length != 0) - { - foreach (var authStrategyId in model.AuthorizationStrategyForReadChanges) - { - if (authStrategyId != 0) - { - if (parentResourceClaims.Any() && parentResourceClaims.SingleOrDefault(x => - x.Action.ActionName == Action.ReadChanges.Value && x.AuthorizationStrategyOverride != null && - x.AuthorizationStrategyOverride.AuthorizationStrategyId == authStrategyId) == null) - { - claimSetResourceClaim.AuthorizationStrategyOverride = authorizationStrategiesDictionary[authStrategyId]; - } - else if (!parentResourceClaims.Any()) - { - claimSetResourceClaim.AuthorizationStrategyOverride = authorizationStrategiesDictionary[authStrategyId]; - } - } - } - - } - } - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/ResourceClaim.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/ResourceClaim.cs index 70879c8f1..b63b58b3c 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/ResourceClaim.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/ResourceClaim.cs @@ -3,30 +3,34 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Ods.AdminApi.Infrastructure.Services.ClaimSetEditor; using Newtonsoft.Json; +using Swashbuckle.AspNetCore.Annotations; -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor; - -public class ResourceClaim +namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor { - public int Id { get; set; } - public int ParentId { get; set; } - public string? ParentName { get; set; } - public string? Name { get; set; } - public bool Create { get; set; } - public bool Read { get; set; } - public bool Update { get; set; } - public bool Delete { get; set; } - public bool ReadChanges { get; set; } - [JsonIgnore] - public bool IsParent { get; set; } - public ClaimSetResourceClaimActionAuthStrategies?[] DefaultAuthStrategiesForCRUD { get; set; } = Array.Empty(); - public ClaimSetResourceClaimActionAuthStrategies?[] AuthStrategyOverridesForCRUD { get; set; } = Array.Empty(); - public List Children { get; set; } = new(); + public class ResourceClaim + { + public int Id { get; set; } + public int ParentId { get; set; } + public string? ParentName { get; set; } + public string? Name { get; set; } + public List? Actions { get; set; } + [JsonIgnore] + public bool IsParent { get; set; } + public List DefaultAuthorizationStrategiesForCRUD { get; set; } = new List(); + public List AuthorizationStrategyOverridesForCRUD { get; set; } = new List(); + public List Children { get; set; } + + public ResourceClaim() + { + Children = new List(); + } + } - public ResourceClaim() + [SwaggerSchema(Title = "ResourceClaimAction")] + public class ResourceClaimAction { - Children = new List(); + public string? Name { get; set; } + public bool Enabled { get; set; } } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommand.cs index 7250f8ffc..78c00f228 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommand.cs @@ -3,39 +3,32 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System.Collections.Generic; +using EdFi.Security.DataAccess.Contexts; namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor { public class UpdateResourcesOnClaimSetCommand { - private readonly IOdsSecurityModelVersionResolver _resolver; - private readonly UpdateResourcesOnClaimSetCommandV53Service _v53Service; - private readonly UpdateResourcesOnClaimSetCommandV6Service _v6Service; + private readonly ISecurityContext _context; + private readonly AddOrEditResourcesOnClaimSetCommand _addOrEditResourcesOnClaimSetCommand; - public UpdateResourcesOnClaimSetCommand(IOdsSecurityModelVersionResolver resolver, - UpdateResourcesOnClaimSetCommandV53Service v53Service, - UpdateResourcesOnClaimSetCommandV6Service v6Service) + public UpdateResourcesOnClaimSetCommand(ISecurityContext context, + AddOrEditResourcesOnClaimSetCommand addOrEditResourcesOnClaimSetCommand) { - _resolver = resolver; - _v53Service = v53Service; - _v6Service = v6Service; + _context = context; + _addOrEditResourcesOnClaimSetCommand = addOrEditResourcesOnClaimSetCommand; } public void Execute(IUpdateResourcesOnClaimSetModel model) { - var securityModel = _resolver.DetermineSecurityModel(); - switch (securityModel) - { - case EdFiOdsSecurityModelCompatibility.ThreeThroughFive or EdFiOdsSecurityModelCompatibility.FiveThreeCqe: - _v53Service.Execute(model); - break; - case EdFiOdsSecurityModelCompatibility.Six: - _v6Service.Execute(model); - break; - default: - throw new EdFiOdsSecurityModelCompatibilityException(securityModel); - } + var resourceClaimsForClaimSet = + _context.ClaimSetResourceClaimActions.Where(x => x.ClaimSet.ClaimSetId == model.ClaimSetId).ToList(); + _context.ClaimSetResourceClaimActions.RemoveRange(resourceClaimsForClaimSet); + _context.SaveChanges(); + + if (model.ResourceClaims == null) return; + + _addOrEditResourcesOnClaimSetCommand.Execute(model.ClaimSetId, model.ResourceClaims); } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommandV53Service.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommandV53Service.cs deleted file mode 100644 index 72ce948c6..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/ClaimSetEditor/UpdateResourcesOnClaimSetCommandV53Service.cs +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; - -using Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts; - -namespace EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor -{ - public class UpdateResourcesOnClaimSetCommandV53Service - { - private readonly ISecurityContext _context; - private readonly AddOrEditResourcesOnClaimSetCommand _addOrEditResourcesOnClaimSetCommand; - - public UpdateResourcesOnClaimSetCommandV53Service(ISecurityContext context, - AddOrEditResourcesOnClaimSetCommand addOrEditResourcesOnClaimSetCommand) - { - _context = context; - _addOrEditResourcesOnClaimSetCommand = addOrEditResourcesOnClaimSetCommand; - } - - public void Execute(IUpdateResourcesOnClaimSetModel model) - { - var resourceClaimsForClaimSet = - _context.ClaimSetResourceClaims.Where(x => x.ClaimSet.ClaimSetId == model.ClaimSetId).ToList(); - _context.ClaimSetResourceClaims.RemoveRange(resourceClaimsForClaimSet); - _context.SaveChanges(); - - if (model.ResourceClaims == null) return; - - _addOrEditResourcesOnClaimSetCommand.Execute(model.ClaimSetId, model.ResourceClaims); - } - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Tenants/TenantService.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Tenants/TenantService.cs new file mode 100644 index 000000000..afe214091 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Tenants/TenantService.cs @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Features.Tenants; +using log4net; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; + +public interface ITenantsService +{ + Task InitializeTenantsAsync(); + Task> GetTenantsAsync(bool fromCache = false); + Task GetTenantByTenantIdAsync(string tenantName); +} + +public class TenantService(IOptionsSnapshot options, + IMemoryCache memoryCache) : ITenantsService +{ + private const string ADMIN_DB_KEY = "EdFi_Admin"; + private const string SECURITY_DB_KEY = "EdFi_Security"; + protected AppSettingsFile _appSettings = options.Value; + private readonly IMemoryCache _memoryCache = memoryCache; + private static readonly ILog _log = LogManager.GetLogger(typeof(TenantService)); + + public async Task InitializeTenantsAsync() + { + var tenants = await GetTenantsAsync(); + //store it in memorycache + await Task.FromResult(_memoryCache.Set(Constants.TenantsCacheKey, tenants)); + } + + public async Task> GetTenantsAsync(bool fromCache = false) + { + List results; + + if (fromCache) + { + results = await GetTenantsFromCacheAsync(); + if (results.Count > 0) + { + return results; + } + } + + results = []; + + if (_appSettings.AppSettings.MultiTenancy) + { + foreach (var tenantConfig in _appSettings.Tenants) + { + /// Admin database + var adminConnectionString = tenantConfig.Value.ConnectionStrings.First(p => p.Key == ADMIN_DB_KEY).Value; + if (!ConnectionStringHelper.ValidateConnectionString(_appSettings.AppSettings.DatabaseEngine!, adminConnectionString)) + { + _log.WarnFormat("Tenant {Key} has an invalid connection string for database {ADMIN_DB_KEY}. Database engine is {engine}", + tenantConfig.Key, ADMIN_DB_KEY, _appSettings.AppSettings.DatabaseEngine); + } + + /// Security database + var securityConnectionString = tenantConfig.Value.ConnectionStrings.First(p => p.Key == SECURITY_DB_KEY).Value; + if (!ConnectionStringHelper.ValidateConnectionString(_appSettings.AppSettings.DatabaseEngine!, securityConnectionString)) + { + _log.WarnFormat("Tenant {Key} has an invalid connection string for database {SECURITY_DB_KEY}. Database engine is {engine}", + tenantConfig.Key, SECURITY_DB_KEY, _appSettings.AppSettings.DatabaseEngine); + } + + results.Add(new TenantModel() + { + TenantName = tenantConfig.Key, + ConnectionStrings = new(adminConnectionString, securityConnectionString) + }); + } + } + else + { + results.Add(new TenantModel() + { + TenantName = Constants.DefaultTenantName, + ConnectionStrings = new TenantModelConnectionStrings + ( + edFiAdminConnectionString: _appSettings.ConnectionStrings.First(p => p.Key == ADMIN_DB_KEY).Value, + edFiSecurityConnectionString: _appSettings.ConnectionStrings.First(p => p.Key == SECURITY_DB_KEY).Value + ) + }); + } + + return results; + } + + public async Task GetTenantByTenantIdAsync(string tenantName) + { + var tenants = await GetTenantsAsync(); + var tenant = tenants.FirstOrDefault(p => p.TenantName.Equals(tenantName, StringComparison.OrdinalIgnoreCase)); + return tenant; + } + + private async Task> GetTenantsFromCacheAsync() + { + var tenants = await Task.FromResult(_memoryCache.Get>(Constants.TenantsCacheKey)); + return tenants ?? []; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/SortingConstants.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/SortingConstants.cs new file mode 100644 index 000000000..885a1d50d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/SortingConstants.cs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Features.Actions; +using EdFi.Ods.AdminApi.Features.Applications; +using EdFi.Ods.AdminApi.Features.AuthorizationStrategies; +using EdFi.Ods.AdminApi.Features.ClaimSets; +using EdFi.Ods.AdminApi.Features.OdsInstanceContext; +using EdFi.Ods.AdminApi.Features.OdsInstanceDerivative; +using EdFi.Ods.AdminApi.Features.ODSInstances; +using EdFi.Ods.AdminApi.Features.Vendors; + +namespace EdFi.Ods.AdminApi.Infrastructure; + +public static class SortingColumns +{ + /// Default columns to filter for each entity: + public const string DefaultNameColumn = "Name"; + public const string DefaultIdColumn = "Id"; + public const string ApplicationNameColumn = nameof(ApplicationModel.ApplicationName); + public const string ApplicationClaimSetNameColumn = nameof(ApplicationModel.ClaimSetName); + public const string ActionUriColumn = nameof(ActionModel.Uri); + public const string AuthorizationStrategyDisplayNameColumn = nameof(AuthorizationStrategyModel.DisplayName); + public const string OdsInstanceContextKeyColumn = nameof(OdsInstanceContextModel.ContextKey); + public const string OdsInstanceContextValueColumn = nameof(OdsInstanceContextModel.ContextValue); + public const string OdsInstanceDerivativeTypeColumn = nameof(OdsInstanceDerivativeModel.DerivativeType); + public const string OdsInstanceDerivativeOdsInstanceIdColumn = nameof(OdsInstanceDerivativeModel.OdsInstanceId); + public const string OdsInstanceInstanceTypeColumn = nameof(OdsInstanceModel.InstanceType); + public const string ResourceClaimParentNameColumn = nameof(ResourceClaimModel.ParentName); + public const string ResourceClaimParentIdColumn = nameof(ResourceClaimModel.ParentId); + public const string VendorCompanyColumn = nameof(VendorModel.Company); + public const string VendorContactNameColumn = nameof(VendorModel.ContactName); + public const string VendorContactEmailColumn = nameof(VendorModel.ContactEmailAddress); + public const string VendorNamespacePrefixesColumn = nameof(VendorModel.NamespacePrefixes); + + + + +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs index d1d7ef162..f9f14effe 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs @@ -2,75 +2,86 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -extern alias Compatability; +using System.Net; using System.Reflection; +using System.Threading.RateLimiting; using EdFi.Admin.DataAccess.Contexts; +using EdFi.Common.Extensions; +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Context; +using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; +using EdFi.Ods.AdminApi.Common.Infrastructure.Providers; +using EdFi.Ods.AdminApi.Common.Infrastructure.Providers.Interfaces; +using EdFi.Ods.AdminApi.Common.Infrastructure.Security; +using EdFi.Ods.AdminApi.Common.Infrastructure.Services; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Features.Connect; using EdFi.Ods.AdminApi.Infrastructure.Documentation; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; using EdFi.Ods.AdminApi.Infrastructure.Security; -using EdFi.Ods.AdminApi.Infrastructure.Api; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; using EdFi.Security.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Infrastructure.Services; +using FluentValidation; using FluentValidation.AspNetCore; +using log4net; +using log4net.Config; using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; -using FluentValidation; -using EdFi.Ods.AdminApi.Helpers; namespace EdFi.Ods.AdminApi.Infrastructure; public static class WebApplicationBuilderExtensions { + private static readonly string[] _value = ["api"]; + public static void AddServices(this WebApplicationBuilder webApplicationBuilder) { - IConfiguration config = webApplicationBuilder.Configuration; + webApplicationBuilder.Services.AddSingleton(); + + var env = webApplicationBuilder.Environment; + var appSettingsPath = Path.Combine(env.ContentRootPath, "appsettings.json"); + webApplicationBuilder.Services.AddSingleton(new FileSystemAppSettingsFileProvider(appSettingsPath)); + + ConfigureRateLimiting(webApplicationBuilder); + ConfigurationManager config = webApplicationBuilder.Configuration; webApplicationBuilder.Services.Configure(config.GetSection("AppSettings")); + EnableMultiTenancySupport(webApplicationBuilder); + + var adminApiMode = config.GetValue("AppSettings:AdminApiMode", AdminApiMode.V2); + Assembly assembly; - var executingAssembly = Assembly.GetExecutingAssembly(); - webApplicationBuilder.Services.AddAutoMapper(executingAssembly, typeof(AdminApiMappingProfile).Assembly); webApplicationBuilder.Services.AddScoped(); - foreach (var type in typeof(IMarkerForEdFiOdsAdminApiManagement).Assembly.GetTypes()) + if (adminApiMode == AdminApiMode.V2) { - if (type.IsClass && !type.IsAbstract && (type.IsPublic || type.IsNestedPublic)) - { - var concreteClass = type; + assembly = Assembly.GetExecutingAssembly(); - var interfaces = concreteClass.GetInterfaces().ToArray(); + webApplicationBuilder.Services.AddAutoMapper( + assembly, + typeof(AdminApiMappingProfile).Assembly + ); - if (concreteClass.Namespace != null) - { - if (concreteClass.Namespace.EndsWith("Database.Commands") || concreteClass.Namespace.EndsWith("Database.Queries") - || concreteClass.Namespace.EndsWith("ClaimSetEditor")) - { - if (interfaces.Length == 1) - { - var serviceType = interfaces.Single(); - if (serviceType.FullName == $"{concreteClass.Namespace}.I{concreteClass.Name}") - webApplicationBuilder.Services.AddTransient(serviceType, concreteClass); - } - else if (interfaces.Length == 0) - { - if (concreteClass.Name.EndsWith("Command") - || concreteClass.Name.EndsWith("Query") - || concreteClass.Name.EndsWith("Service")) - { - webApplicationBuilder.Services.AddTransient(concreteClass); - } - } - } - } - } + var adminApiV2Types = typeof(IMarkerForEdFiOdsAdminApiManagement).Assembly.GetTypes(); + RegisterAdminApiServices(webApplicationBuilder, adminApiV2Types); } - - //Add service to identify ODS Version - webApplicationBuilder.Services.AddSingleton(sp => + else { - var odsApiVersion = webApplicationBuilder.Configuration.GetValue("AppSettings:OdsApiVersion"); - return new OdsSecurityVersionResolver(odsApiVersion); - }); + assembly = Assembly.Load("EdFi.Ods.AdminApi.V1"); + + webApplicationBuilder.Services.AddAutoMapper( + assembly, + typeof(V1.Infrastructure.AutoMapper.AdminApiMappingProfile).Assembly + ); + + var adminApiV1Types = typeof(V1.Infrastructure.IMarkerForEdFiOdsAdminApiManagement).Assembly.GetTypes(); + RegisterAdminApiServices(webApplicationBuilder, adminApiV1Types); + } // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle @@ -81,10 +92,18 @@ public static void AddServices(this WebApplicationBuilder webApplicationBuilder) opt.AssumeDefaultVersionWhenUnspecified = false; }); + webApplicationBuilder.Services.Configure(config.GetSection("SwaggerSettings")); var issuer = webApplicationBuilder.Configuration.GetValue("Authentication:IssuerUrl"); webApplicationBuilder.Services.AddSwaggerGen(opt => { - opt.CustomSchemaIds(x => x.FullName?.Replace("+", ".")); + opt.EnableAnnotations(); + opt.CustomSchemaIds(x => + { + var name = x.FullName?.Replace(x.Namespace + ".", ""); + if (name != null && name.Any(c => c == '+')) + name = name.Split('+')[1]; + return name.ToCamelCase(); + }); opt.OperationFilter(); opt.OperationFilter(); opt.AddSecurityDefinition( @@ -96,10 +115,10 @@ public static void AddServices(this WebApplicationBuilder webApplicationBuilder) ClientCredentials = new OpenApiOAuthFlow { TokenUrl = new Uri($"{issuer}/{SecurityConstants.TokenEndpoint}"), - Scopes = new Dictionary - { - { SecurityConstants.Scopes.AdminApiFullAccess, "Unrestricted access to all Admin API endpoints" }, - } + Scopes = SecurityConstants.Scopes.AllScopes.ToDictionary( + x => x.Scope, + x => x.ScopeDescription + ), }, }, In = ParameterLocation.Header, @@ -114,26 +133,37 @@ public static void AddServices(this WebApplicationBuilder webApplicationBuilder) new OpenApiSecurityScheme { Reference = new OpenApiReference - { Type = ReferenceType.SecurityScheme, Id = "oauth" }, + { + Type = ReferenceType.SecurityScheme, + Id = "oauth" + }, }, - new[] { "api" } + _value } } ); foreach (var version in AdminApiVersions.GetAllVersionStrings()) { - opt.SwaggerDoc(version, new OpenApiInfo - { - Title = "Admin API Documentation", Version = version - }); + opt.SwaggerDoc( + version, + new OpenApiInfo + { + Title = "Admin API Documentation", + Description = + "The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API.", + Version = version + } + ); } opt.DocumentFilter(); opt.SchemaFilter(); + opt.SchemaFilter(); opt.SchemaFilter(); opt.OperationFilter(); - opt.EnableAnnotations(); + opt.OperationFilter(); + opt.OrderActionsBy(x => { return x.HttpMethod != null && Enum.TryParse(x.HttpMethod, out var verb) @@ -142,24 +172,24 @@ public static void AddServices(this WebApplicationBuilder webApplicationBuilder) }); }); - // Logging - var loggingOptions = webApplicationBuilder.Configuration.GetSection("Log4NetCore").Get(); - webApplicationBuilder.Logging.AddLog4Net(loggingOptions); - // Fluent validation - webApplicationBuilder.Services - .AddValidatorsFromAssembly(executingAssembly) + webApplicationBuilder + .Services.AddValidatorsFromAssembly(assembly) .AddFluentValidationAutoValidation(); - ValidatorOptions.Global.DisplayNameResolver = (type, memberInfo, expression) - => memberInfo? - .GetCustomAttribute()?.GetName(); + + webApplicationBuilder.Services.AddTransient(); + + ValidatorOptions.Global.DisplayNameResolver = (type, memberInfo, expression) => + memberInfo + ?.GetCustomAttribute() + ?.GetName(); //Databases - var databaseEngine = webApplicationBuilder.Configuration["AppSettings:DatabaseEngine"]; - var (connectionString, isSqlServer) = webApplicationBuilder.AddDatabases(databaseEngine); + var databaseEngine = config.Get("AppSettings:DatabaseEngine", "SqlServer"); + webApplicationBuilder.AddDatabases(databaseEngine); //Health - webApplicationBuilder.Services.AddHealthCheck(connectionString, isSqlServer); + webApplicationBuilder.Services.AddHealthCheck(webApplicationBuilder.Configuration); //JSON webApplicationBuilder.Services.Configure(o => @@ -167,76 +197,213 @@ public static void AddServices(this WebApplicationBuilder webApplicationBuilder) o.SerializerOptions.WriteIndented = true; }); - webApplicationBuilder.Services.AddSecurityUsingOpenIddict(webApplicationBuilder.Configuration, webApplicationBuilder.Environment); + webApplicationBuilder.Services.AddSecurityUsingOpenIddict( + webApplicationBuilder.Configuration, + webApplicationBuilder.Environment + ); webApplicationBuilder.Services.AddHttpClient(); + webApplicationBuilder.Services.AddTransient(); webApplicationBuilder.Services.AddTransient(); + webApplicationBuilder.Services.Configure(webApplicationBuilder.Configuration); + webApplicationBuilder.Services.AddTransient(); } - private static (string adminConnectionString, bool) AddDatabases(this WebApplicationBuilder webApplicationBuilder, string databaseEngine) + public static void AddLoggingServices(this WebApplicationBuilder webApplicationBuilder) { - var adminConnectionString = webApplicationBuilder.Configuration.GetConnectionString("Admin"); - var securityConnectionString = webApplicationBuilder.Configuration.GetConnectionString("Security"); + ConfigurationManager config = webApplicationBuilder.Configuration; - if (DatabaseEngineEnum.Parse(databaseEngine).Equals(DatabaseEngineEnum.PostgreSql)) - { - webApplicationBuilder.Services.AddDbContext( - options => - { - options.UseNpgsql(adminConnectionString); - options.UseOpenIddict(); - }); + // Remove all default logging providers (Console, Debug, etc.) + webApplicationBuilder.Logging.ClearProviders(); - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(securityConnectionString); - optionsBuilder.UseLowerCaseNamingConvention(); - var context = new PostgresSecurityContext(optionsBuilder.Options); + // Initialize log4net early so we can use it in Program.cs + var log4netConfigFileName = webApplicationBuilder.Configuration.GetValue("Log4NetCore:Log4NetConfigFileName"); + if (!string.IsNullOrEmpty(log4netConfigFileName)) + { + var log4netConfigPath = Path.Combine(AppContext.BaseDirectory, log4netConfigFileName); + if (File.Exists(log4netConfigPath)) + { + var log4netConfig = new FileInfo(log4netConfigPath); + XmlConfigurator.Configure(LogManager.GetRepository(), log4netConfig); + } + } - webApplicationBuilder.Services.AddScoped( - sp => new Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts.PostgresSecurityContext(optionsBuilder.Options)); + // Important to display messages based on the Logging section in appsettings.json + var loggingOptions = config.GetSection("Log4NetCore").Get(); + webApplicationBuilder.Logging.AddLog4Net(loggingOptions); + } - webApplicationBuilder.Services.AddScoped( - sp => new PostgresSecurityContext(SecurityDbContextOptions(DatabaseEngineEnum.PostgreSql))); + private static void EnableMultiTenancySupport(this WebApplicationBuilder webApplicationBuilder) + { + webApplicationBuilder.Services.AddTransient< + ITenantConfigurationProvider, + TenantConfigurationProvider + >(); + webApplicationBuilder.Services.AddTransient< + IContextProvider, + ContextProvider + >(); + webApplicationBuilder.Services.AddSingleton(); + webApplicationBuilder.Services.AddScoped(); + webApplicationBuilder.Services.Configure(webApplicationBuilder.Configuration); + } - webApplicationBuilder.Services.AddScoped( - sp => new PostgresUsersContext(AdminDbContextOptions(DatabaseEngineEnum.PostgreSql))); + private static void AddDatabases(this WebApplicationBuilder webApplicationBuilder, string databaseEngine) + { + IConfiguration config = webApplicationBuilder.Configuration; - return (adminConnectionString, false); - } + var adminApiMode = config.GetValue("AppSettings:AdminApiMode", AdminApiMode.V2); + var multiTenancyEnabled = config.Get("AppSettings:MultiTenancy", false); - if (DatabaseEngineEnum.Parse(databaseEngine).Equals(DatabaseEngineEnum.SqlServer)) + switch (adminApiMode) { - webApplicationBuilder.Services.AddDbContext( - options => + case AdminApiMode.V1: + if (DatabaseEngineEnum.Parse(databaseEngine).Equals(DatabaseEngineEnum.PostgreSql)) { - options.UseSqlServer(adminConnectionString); - options.UseOpenIddict(); - }); + var adminConnectionString = webApplicationBuilder.Configuration.GetConnectionString("EdFi_Admin"); + var securityConnectionString = webApplicationBuilder.Configuration.GetConnectionString("EdFi_Security"); + + webApplicationBuilder.Services.AddDbContext( + options => + { + options.UseNpgsql(adminConnectionString); + options.UseOpenIddict(); + }); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(securityConnectionString); + optionsBuilder.UseLowerCaseNamingConvention(); - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlServer(securityConnectionString); - var context = new SqlServerSecurityContext(optionsBuilder.Options); + webApplicationBuilder.Services.AddScoped( + sp => new V1.Security.DataAccess.Contexts.PostgresSecurityContext(SecurityDbContextOptions(sp, DatabaseEngineEnum.PostgreSql))); - webApplicationBuilder.Services.AddScoped( - sp => new Compatability::EdFi.SecurityCompatiblity53.DataAccess.Contexts.SqlServerSecurityContext(optionsBuilder.Options)); + webApplicationBuilder.Services.AddScoped( + sp => new V1.Admin.DataAccess.Contexts.PostgresUsersContext(AdminDbContextOptions(sp, DatabaseEngineEnum.PostgreSql))); + } + else if (DatabaseEngineEnum.Parse(databaseEngine).Equals(DatabaseEngineEnum.SqlServer)) + { + var adminConnectionString = webApplicationBuilder.Configuration.GetConnectionString("EdFi_Admin"); + var securityConnectionString = webApplicationBuilder.Configuration.GetConnectionString("EdFi_Security"); + + webApplicationBuilder.Services.AddDbContext( + options => + { + options.UseSqlServer(adminConnectionString); + options.UseOpenIddict(); + }); - webApplicationBuilder.Services.AddScoped( - sp => new SqlServerSecurityContext(SecurityDbContextOptions(DatabaseEngineEnum.SqlServer))); + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer(securityConnectionString); - webApplicationBuilder.Services.AddScoped( - sp => new SqlServerUsersContext(AdminDbContextOptions(DatabaseEngineEnum.SqlServer))); + webApplicationBuilder.Services.AddScoped( + sp => new V1.Security.DataAccess.Contexts.SqlServerSecurityContext(SecurityDbContextOptions(sp, DatabaseEngineEnum.SqlServer))); - return (adminConnectionString, true); + webApplicationBuilder.Services.AddScoped( + sp => new V1.Admin.DataAccess.Contexts.SqlServerUsersContext(AdminDbContextOptions(sp, DatabaseEngineEnum.SqlServer))); + } + else + { + throw new ArgumentException( + $"Unexpected DB setup error. Engine '{databaseEngine}' was parsed as valid but is not configured for startup." + ); + } + break; + case AdminApiMode.V2: + if (DatabaseEngineEnum.Parse(databaseEngine).Equals(DatabaseEngineEnum.PostgreSql)) + { + webApplicationBuilder.Services.AddDbContext( + (sp, options) => + { + options.UseNpgsql( + AdminConnectionString(sp), + o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) + ); + options.UseLowerCaseNamingConvention(); + options.UseOpenIddict(); + } + ); + + webApplicationBuilder.Services.AddScoped(sp => new PostgresSecurityContext( + SecurityDbContextOptions(sp, DatabaseEngineEnum.PostgreSql) + )); + + webApplicationBuilder.Services.AddScoped( + sp => new AdminConsolePostgresUsersContext( + AdminDbContextOptions(sp, DatabaseEngineEnum.PostgreSql) + ) + ); + } + else if (DatabaseEngineEnum.Parse(databaseEngine).Equals(DatabaseEngineEnum.SqlServer)) + { + webApplicationBuilder.Services.AddDbContext( + (sp, options) => + { + options.UseSqlServer( + AdminConnectionString(sp), + o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) + ); + options.UseOpenIddict(); + } + ); + + webApplicationBuilder.Services.AddScoped( + (sp) => + new SqlServerSecurityContext(SecurityDbContextOptions(sp, DatabaseEngineEnum.SqlServer)) + ); + + webApplicationBuilder.Services.AddScoped( + (sp) => + new AdminConsoleSqlServerUsersContext( + AdminDbContextOptions(sp, DatabaseEngineEnum.SqlServer) + ) + ); + } + else + { + throw new ArgumentException( + $"Unexpected DB setup error. Engine '{databaseEngine}' was parsed as valid but is not configured for startup." + ); + } + break; + default: + throw new InvalidOperationException($"Invalid adminApiMode: {adminApiMode}. Must be 'v1' or 'v2'"); } - throw new Exception($"Unexpected DB setup error. Engine '{databaseEngine}' was parsed as valid but is not configured for startup."); + string AdminConnectionString(IServiceProvider serviceProvider) + { + var adminConnectionString = string.Empty; - DbContextOptions AdminDbContextOptions(string databaseEngine) + if (multiTenancyEnabled) + { + var tenant = serviceProvider + .GetRequiredService>() + .Get(); + if (tenant != null && !string.IsNullOrEmpty(tenant.AdminConnectionString)) + { + adminConnectionString = tenant.AdminConnectionString; + } + else + { + throw new ArgumentException( + $"Admin database connection setup error. Tenant not configured correctly." + ); + } + } + else + { + adminConnectionString = config.GetConnectionStringByName("EdFi_Admin"); + } + + return adminConnectionString; + } + + DbContextOptions AdminDbContextOptions(IServiceProvider serviceProvider, string databaseEngine) { - DbContextOptionsBuilder builder = new DbContextOptionsBuilder(); + var adminConnectionString = AdminConnectionString(serviceProvider); + var builder = new DbContextOptionsBuilder(); if (DatabaseEngineEnum.Parse(databaseEngine).Equals(DatabaseEngineEnum.PostgreSql)) { builder.UseNpgsql(adminConnectionString); @@ -249,9 +416,38 @@ DbContextOptions AdminDbContextOptions(string databaseEngine) return builder.Options; } - DbContextOptions SecurityDbContextOptions(string databaseEngine) + string SecurityConnectionString(IServiceProvider serviceProvider) + { + var securityConnectionString = string.Empty; + + if (multiTenancyEnabled) + { + var tenant = serviceProvider + .GetRequiredService>() + .Get(); + if (tenant != null && !string.IsNullOrEmpty(tenant.SecurityConnectionString)) + { + securityConnectionString = tenant.SecurityConnectionString; + } + else + { + throw new ArgumentException( + $"Security database connection setup error. Tenant not configured correctly." + ); + } + } + else + { + securityConnectionString = config.GetConnectionStringByName("EdFi_Security"); + } + + return securityConnectionString; + } + + DbContextOptions SecurityDbContextOptions(IServiceProvider serviceProvider, string databaseEngine) { - DbContextOptionsBuilder builder = new DbContextOptionsBuilder(); + var securityConnectionString = SecurityConnectionString(serviceProvider); + var builder = new DbContextOptionsBuilder(); if (DatabaseEngineEnum.Parse(databaseEngine).Equals(DatabaseEngineEnum.PostgreSql)) { builder.UseNpgsql(securityConnectionString); @@ -266,6 +462,113 @@ DbContextOptions SecurityDbContextOptions(string databaseEngine) } } + private static void RegisterAdminApiServices(WebApplicationBuilder webApplicationBuilder, Type[] types) + { + foreach (var type in types) + { + if (type.IsClass && !type.IsAbstract && (type.IsPublic || type.IsNestedPublic)) + { + var concreteClass = type; + + var interfaces = concreteClass.GetInterfaces().ToArray(); + + if (concreteClass.Namespace is not null) + { + if ( + !concreteClass.Namespace.EndsWith("Database.Commands") + && !concreteClass.Namespace.EndsWith("Database.Queries") + && !concreteClass.Namespace.EndsWith("ClaimSetEditor") + ) + { + continue; + } + + if (interfaces.Length == 1) + { + var serviceType = interfaces.Single(); + if (serviceType.FullName == $"{concreteClass.Namespace}.I{concreteClass.Name}") + { + webApplicationBuilder.Services.AddTransient(serviceType, concreteClass); + } + } + else if (interfaces.Length == 0) + { + if ( + !concreteClass.Name.EndsWith("Command") + && !concreteClass.Name.EndsWith("Query") + && !concreteClass.Name.EndsWith("Service") + ) + { + continue; + } + webApplicationBuilder.Services.AddTransient(concreteClass); + } + } + } + } + } + + public static void ConfigureRateLimiting(WebApplicationBuilder builder) + { + // Bind IpRateLimiting section + builder.Services.Configure(builder.Configuration.GetSection("IpRateLimiting")); + + // Add new rate limiting policy using config + builder.Services.AddRateLimiter(options => + { + var config = builder.Configuration.GetSection("IpRateLimiting").Get(); + + if (config == null || !config.EnableEndpointRateLimiting) + { + options.GlobalLimiter = PartitionedRateLimiter.Create(_ => RateLimitPartition.GetNoLimiter("none")); + return; + } + // Set global options + options.RejectionStatusCode = config?.HttpStatusCode ?? (int)HttpStatusCode.TooManyRequests; + + if (config?.GeneralRules != null) + { + foreach (var rule in config.GeneralRules) + { + // Only support fixed window for now, parse period (e.g., "1m") + var window = rule.Period.EndsWith('m') ? TimeSpan.FromMinutes(int.Parse(rule.Period.TrimEnd('m'))) : TimeSpan.FromMinutes(1); + // Register a named limiter for each endpoint + options.AddFixedWindowLimiter(rule.Endpoint, _ => new FixedWindowRateLimiterOptions + { + PermitLimit = rule.Limit, + Window = window, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 0 + }); + } + // Use a global policy selector to apply endpoint-specific limiters + options.GlobalLimiter = PartitionedRateLimiter.Create(context => + { + var path = context.Request.Path.Value; + var method = context.Request.Method; + foreach (var rule in config.GeneralRules) + { + var parts = rule.Endpoint.Split(':'); + // Only support fixed window for now, parse period (e.g., "1m") + var window = rule.Period.EndsWith('m') ? TimeSpan.FromMinutes(int.Parse(rule.Period.TrimEnd('m'))) : TimeSpan.FromMinutes(1); + if (path != null && parts.Length == 2 && method.Equals(parts[0], StringComparison.OrdinalIgnoreCase) && path.Equals(parts[1], StringComparison.OrdinalIgnoreCase)) + { + return RateLimitPartition.GetFixedWindowLimiter(rule.Endpoint, _ => new FixedWindowRateLimiterOptions + { + PermitLimit = rule.Limit, + Window = window, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 0 + }); + } + } + // No limiter for this endpoint + return RateLimitPartition.GetNoLimiter("none"); + }); + } + }); + } + private enum HttpVerbOrder { GET = 1, diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationExtensions.cs index 7107c1ced..bef144208 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationExtensions.cs @@ -2,6 +2,9 @@ // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.AdminApi.Common.Constants; +using AdminApiV1Features = EdFi.Ods.AdminApi.V1.Infrastructure.Helpers; +using AdminApiV2Features = EdFi.Ods.AdminApi.Infrastructure.Helpers; namespace EdFi.Ods.AdminApi.Infrastructure; @@ -9,13 +12,28 @@ public static class WebApplicationExtensions { public static void MapFeatureEndpoints(this WebApplication application) { - application.UseEndpoints(endpoints => + var adminApiMode = application.Configuration.GetValue("AppSettings:adminApiMode", AdminApiMode.V2); + + switch (adminApiMode) { - foreach (var routeBuilder in Helpers.FeaturesHelper.GetFeatures()) - { - routeBuilder.MapEndpoints(endpoints); - } - }); + case AdminApiMode.V1: + foreach (var routeBuilder in AdminApiV1Features.AdminApiV1FeatureHelper.GetFeatures()) + { + routeBuilder.MapEndpoints(application); + } + new Features.Information.ReadInformation().MapEndpoints(application); + break; + + case AdminApiMode.V2: + foreach (var routeBuilder in AdminApiV2Features.AdminApiFeatureHelper.GetFeatures()) + { + routeBuilder.MapEndpoints(application); + } + break; + + default: + throw new InvalidOperationException($"Invalid adminApiMode: {adminApiMode}"); + } } public static void DefineSwaggerUIWithApiVersions(this WebApplication application, params string[] versions) diff --git a/Application/EdFi.Ods.AdminApi/Program.cs b/Application/EdFi.Ods.AdminApi/Program.cs index 8af3386d7..a54c5d20a 100644 --- a/Application/EdFi.Ods.AdminApi/Program.cs +++ b/Application/EdFi.Ods.AdminApi/Program.cs @@ -1,44 +1,87 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Ods.AdminApi.Features; -using EdFi.Ods.AdminApi.Infrastructure; -using log4net; - -var builder = WebApplication.CreateBuilder(args); - -builder.AddServices(); - -// logging -var _logger = LogManager.GetLogger("Program"); -_logger.Info("Starting Admin API"); - -var app = builder.Build(); - -var pathBase = app.Configuration.GetValue("AppSettings:PathBase"); -if (!string.IsNullOrEmpty(pathBase)) -{ - app.UsePathBase("/" + pathBase.Trim('/')); - app.UseForwardedHeaders(); -} - -AdminApiVersions.Initialize(app); - -//The ordering here is meaningful: Logging -> Routing -> Auth -> Endpoints -app.UseMiddleware(); -app.UseRouting(); -app.UseAuthentication(); -app.UseAuthorization(); -app.MapFeatureEndpoints(); -app.MapControllers(); -app.UseHealthChecks("/health"); - -if (app.Configuration.GetValue("EnableSwagger")) -{ - app.UseSwagger(); - app.DefineSwaggerUIWithApiVersions(AdminApiVersions.GetAllVersionStrings()); -} - -app.Run(); +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; +using EdFi.Ods.AdminApi.Features; +using EdFi.Ods.AdminApi.Infrastructure; +using log4net; +using log4net.Config; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +var builder = WebApplication.CreateBuilder(args); + +// Initialize log4net early so we can use it in Program.cs +builder.AddLoggingServices(); + +// logging +var _logger = LogManager.GetLogger("Program"); +_logger.Info("Starting Admin API"); +var adminApiMode = builder.Configuration.GetValue("AppSettings:AdminApiMode", AdminApiMode.V2); +var databaseEngine = builder.Configuration.GetValue("AppSettings:DatabaseEngine"); + +// Log configuration values as requested +_logger.InfoFormat("Configuration - ApiMode: {0}, Engine: {1}", adminApiMode, databaseEngine); + +builder.AddServices(); + +var app = builder.Build(); + +var pathBase = app.Configuration.GetValue("AppSettings:PathBase"); +if (!string.IsNullOrEmpty(pathBase)) +{ + app.UsePathBase($"/{pathBase.Trim('/')}"); + app.UseForwardedHeaders(); +} + +AdminApiVersions.Initialize(app); + +//The ordering here is meaningful: Logging -> Routing -> Auth -> Endpoints +app.UseMiddleware(); +app.UseMiddleware(); + +if (adminApiMode == AdminApiMode.V2) + app.UseMiddleware(); + +app.UseRouting(); +app.UseAuthentication(); +app.UseRateLimiter(); +app.UseAuthorization(); +app.MapFeatureEndpoints(); + +app.MapControllers(); +app.UseHealthChecks("/health", new HealthCheckOptions +{ + ResponseWriter = async (context, report) => + { + context.Response.ContentType = "application/json"; + + // 200 OK if all are healthy, 503 Service Unavailable if any are unhealthy + context.Response.StatusCode = report.Status == HealthStatus.Unhealthy ? (int)HttpStatusCode.ServiceUnavailable : (int)HttpStatusCode.OK; + + var response = new + { + Status = report.Status.ToString(), + Results = report.Entries.GroupBy(x => x.Value.Tags.FirstOrDefault()).Select(x => new + { + Name = x.Key, + Status = x.Min(y => y.Value.Status).ToString() + }) + }; + + await context.Response.WriteAsJsonAsync(response); + } +}); + +if (app.Configuration.GetValue("SwaggerSettings:EnableSwagger")) +{ + app.UseSwagger(); + app.DefineSwaggerUIWithApiVersions(AdminApiVersions.GetAllVersionStrings()); +} + +await app.RunAsync(); diff --git a/Application/EdFi.Ods.AdminApi/Properties/launchSettings.json b/Application/EdFi.Ods.AdminApi/Properties/launchSettings.json index b2bd084f9..5580daf38 100644 --- a/Application/EdFi.Ods.AdminApi/Properties/launchSettings.json +++ b/Application/EdFi.Ods.AdminApi/Properties/launchSettings.json @@ -17,7 +17,6 @@ "applicationUrl": "https://localhost:7214;http://localhost:5214", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "AUTHENTICATION__AUTHORITY": "https://localhost:7214", "AUTHENTICATION__ISSUERURL": "https://localhost:7214" } }, @@ -27,14 +26,13 @@ "launchBrowser": true, "launchUrl": "swagger", "applicationUrl": "https://localhost:7214;http://localhost:5214", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Production", - "AUTHENTICATION__AUTHORITY": "https://localhost:7214", - "AUTHENTICATION__ISSUERURL": "https://localhost:7214", - "AUTHENTICATION__SIGNINGKEY": "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=", - "AUTHENTICATION__ALLOWREGISTRATION": "true", - "ENABLESWAGGER": "true" - } + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "AUTHENTICATION__ISSUERURL": "https://localhost:7214", + "AUTHENTICATION__SIGNINGKEY": "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=", + "AUTHENTICATION__ALLOWREGISTRATION": "true", + "SWAGGERSETTINGS__ENABLESWAGGER": "true" + } }, "EdFi.Ods.AdminApi (Docker)": { "commandName": "Docker", @@ -52,7 +50,6 @@ "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "AUTHENTICATION__AUTHORITY": "https://localhost:44370", "AUTHENTICATION__ISSUERURL": "https://localhost:44370" } } diff --git a/Application/EdFi.Ods.AdminApi/Schema/Ed-Fi-ODS-API-Profile.xsd b/Application/EdFi.Ods.AdminApi/Schema/Ed-Fi-ODS-API-Profile.xsd new file mode 100644 index 000000000..c7541df4c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Schema/Ed-Fi-ODS-API-Profile.xsd @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Application/EdFi.Ods.AdminApi/appsettings.Development.json b/Application/EdFi.Ods.AdminApi/appsettings.Development.json index 0e471c02f..589118bda 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.Development.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.Development.json @@ -1,12 +1,54 @@ { - "Authentication": { - "AllowRegistration": true - }, - "EnableSwagger": true, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "AppSettings": { + "MultiTenancy": true, + "DatabaseEngine": "SqlServer", + "IgnoresCertificateErrors": true + }, + "AdminConsoleSettings": { + "CorsSettings": { + "EnableCors": true, + "AllowedOrigins": [ + "http://localhost:8598", + "https://localhost:8598" + ] + } + }, + "Authentication": { + "IssuerUrl": "", + "SigningKey": "", + "ValidateIssuerSigningKey": true, + "RoleClaimAttribute": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", + "AllowRegistration": true + }, + "ConnectionStrings": { + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin7;Integrated Security=True;Trusted_Connection=true;Encrypt=True;TrustServerCertificate=True", + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security7;Integrated Security=True;Trusted_Connection=true;Encrypt=True;TrustServerCertificate=True" + }, + "SwaggerSettings": { + "EnableSwagger": true, + "DefaultTenant": "tenant1" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Tenants": { + "tenant1": { + "ConnectionStrings": { + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security7;Integrated Security=True;Trusted_Connection=true;Encrypt=True;TrustServerCertificate=True", + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin7;Integrated Security=True;Trusted_Connection=true;Encrypt=True;TrustServerCertificate=True" + } + }, + "tenant2": { + "ConnectionStrings": { + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security7;Integrated Security=True;Trusted_Connection=true;Encrypt=True;TrustServerCertificate=True", + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin7;Integrated Security=True;Trusted_Connection=true;Encrypt=True;TrustServerCertificate=True" + } + } + }, + "Testing": { + "InjectException": false } - } } diff --git a/Application/EdFi.Ods.AdminApi/appsettings.Docker.mssql.json b/Application/EdFi.Ods.AdminApi/appsettings.Docker.mssql.json deleted file mode 100644 index ea5ee42cc..000000000 --- a/Application/EdFi.Ods.AdminApi/appsettings.Docker.mssql.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "AppSettings": { - "DefaultPageSizeOffset": 0, - "DefaultPageSizeLimit": 25, - "DatabaseEngine": "SqlServer", - "ApiStartupType": "$API_MODE", - "PathBase": "$ADMIN_API_VIRTUAL_NAME", - "OdsApiVersion": "$ODS_API_VERSION" - }, - "Authentication": { - "Authority": "$AUTHORITY", - "IssuerUrl": "$ISSUER_URL", - "SigningKey": "$SIGNING_KEY", - "AllowRegistration": true - }, - "EnableSwagger": true, - "EnableDockerEnvironment": true, - "ConnectionStrings": { - "Admin": "Data Source=$SQLSERVER_ADMIN_DATASOURCE;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true", - "Security": "Data Source=$SQLSERVER_SECURITY_DATASOURCE;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - }, - "Log4NetCore": { - "Log4NetConfigFileName": "./log4net.config" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information", - "Microsoft.AspNetCore.DataProtection": "Information" - } - }, - "AllowedHosts": "*" -} diff --git a/Application/EdFi.Ods.AdminApi/appsettings.Docker.pgsql.json b/Application/EdFi.Ods.AdminApi/appsettings.Docker.pgsql.json deleted file mode 100644 index 1d3f127eb..000000000 --- a/Application/EdFi.Ods.AdminApi/appsettings.Docker.pgsql.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "AppSettings": { - "DefaultPageSizeOffset": 0, - "DefaultPageSizeLimit": 25, - "DatabaseEngine": "PostgreSQL", - "ApiStartupType": "$API_MODE", - "PathBase": "$ADMIN_API_VIRTUAL_NAME", - "OdsApiVersion": "$ODS_API_VERSION" - }, - "Authentication": { - "Authority": "$AUTHORITY", - "IssuerUrl": "$ISSUER_URL", - "SigningKey": "$SIGNING_KEY", - "AllowRegistration": true - }, - "EnableSwagger": true, - "EnableDockerEnvironment": true, - "ConnectionStrings": { - "Admin": "host=${ADMIN_POSTGRES_HOST};port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=false", - "Security": "host=${ADMIN_POSTGRES_HOST};port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=false" - }, - "Log4NetCore": { - "Log4NetConfigFileName": "./log4net.config" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information", - "Microsoft.AspNetCore.DataProtection": "Information" - } - }, - "AllowedHosts": "*" -} diff --git a/Application/EdFi.Ods.AdminApi/appsettings.json b/Application/EdFi.Ods.AdminApi/appsettings.json index d71471f92..7e3a8c281 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.json @@ -1,35 +1,90 @@ { - "AppSettings": { - "DatabaseEngine": "SqlServer", - "ProductionApiUrl": "http://localhost:54746", - "ApiStartupType": "SharedInstance", - "PathBase": "", - "OdsApiVersion": "6.1", - "DefaultPageSizeOffset": 0, - "DefaultPageSizeLimit": 25 - }, - "Authentication": { - "Authority": "", - "IssuerUrl": "", - "SigningKey": "", - "AllowRegistration": false + "AppSettings": { + "DatabaseEngine": "SqlServer", + "EncryptionKey": "{ BASE_64_ENCRYPTION_KEY }", + "PathBase": "", + "DefaultPageSizeOffset": 0, + "DefaultPageSizeLimit": 25, + "MultiTenancy": false, + "PreventDuplicateApplications": false, + "EnableApplicationResetEndpoint": false, + "adminApiMode": "v2" + }, + "AdminConsoleSettings": { + "ApplicationName": "Ed-Fi Health Check", + "ClaimsetName": "Ed-Fi Admin App", + "VendorCompany": "Ed-Fi Administrative Tools", + "VendorContactName": "", + "VendorNamespacePrefixes": "uri://ed-fi.org", + "CorsSettings": { + "EnableCors": false, + "AllowedOrigins": [ + "https://localhost" + ] }, + "EncryptionKey": "abcdefghi" + }, + "Authentication": { + "IssuerUrl": "", + "SigningKey": "", + "ValidateIssuerSigningKey": true, + "RoleClaimAttribute": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", + "AllowRegistration": true, + "Authority": null + }, + "SwaggerSettings": { "EnableSwagger": false, - "EnableDockerEnvironment": false, - "ConnectionStrings": { - "Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin;Integrated Security=True;Encrypt=False", - "Security": "Data Source=.\\;Initial Catalog=EdFi_Security;Integrated Security=True;Encrypt=False" - }, - "Log4NetCore": { - "Log4NetConfigFileName": "log4net\\log4net.config" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "OpenIddict.*": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } + "DefaultTenant": "" + }, + "EnableDockerEnvironment": false, + "ConnectionStrings": { + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin;Integrated Security=True", + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security;Integrated Security=True" + }, + "EdFiApiDiscoveryUrl": "https://api.ed-fi.org/v7.2/api/", + "Log4NetCore": { + "Log4NetConfigFileName": "log4net/log4net.config" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "OpenIddict.*": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Tenants": { + "tenant1": { + "ConnectionStrings": { + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security_Tenant1;Integrated Security=True", + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin_Tenant1;Integrated Security=True" + } }, - "AllowedHosts": "*" + "tenant2": { + "ConnectionStrings": { + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security_Tenant2;Integrated Security=True", + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin_Tenant2;Integrated Security=True" + } + } + }, + "AllowedHosts": "*", + "IpRateLimiting": { + "EnableEndpointRateLimiting": true, + "StackBlockedRequests": false, + "RealIpHeader": "X-Real-IP", + "ClientIdHeader": "X-ClientId", + "HttpStatusCode": 429, + "IpWhitelist": [], + "EndpointWhitelist": [], + "GeneralRules": [ + { + "Endpoint": "POST:/Connect/Register", + "Period": "1m", + "Limit": 3 + } + ] + }, + "Testing": { + "InjectException": false + } } diff --git a/Application/EdFi.Ods.AdminApi/env.example.dev b/Application/EdFi.Ods.AdminApi/env.example.dev index 9de063d5d..da911813d 100644 --- a/Application/EdFi.Ods.AdminApi/env.example.dev +++ b/Application/EdFi.Ods.AdminApi/env.example.dev @@ -1,12 +1,10 @@ API_MODE= ODS_VIRTUAL_NAME= ADMIN_API_VIRTUAL_NAME= -ODS_API_VERSION= # For Authentication -AUTHORITY= ISSUER_URL= -SIGNING_KEY= +SIGNING_KEY= # For Postgres only POSTGRES_USER= @@ -15,3 +13,9 @@ POSTGRES_PORT= ADMIN_POSTGRES_HOST= - + - + @@ -18,7 +18,7 @@ - + diff --git a/Application/NuGet.Config b/Application/NuGet.Config index c8c7f1281..a342d405a 100644 --- a/Application/NuGet.Config +++ b/Application/NuGet.Config @@ -4,6 +4,17 @@ + + + + + + + + + + + diff --git a/BuildAdminApiDockerDevelopment.ps1 b/BuildAdminApiDockerDevelopment.ps1 deleted file mode 100644 index 4fb4881c4..000000000 --- a/BuildAdminApiDockerDevelopment.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Licensed to the Ed-Fi Alliance under one or more agreements. -# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -# See the LICENSE and NOTICES files in the project root for more information. - -<# - .SYNOPSIS - Script for running the build operation and copy over the latest files to an existing AdminApi docker container for testing. - - .DESCRIPTION - Script for facilitating the local docker testing with latest changes. Developer can set the required appsettings values and trigger - the build. Once the build done, the apsettings.json will be updated with values provided and - latest files will be copied over to an existing AdminApi docker container folder. - - .EXAMPLE - .\BuildAdminApiDockerDevelopment.ps1 -#> - -$p = @{ - ProductionApiUrl = "http://api" - ApiStartupType = "SharedInstance" - DatabaseEngine = "PostgreSql" - PathBase = "adminapi" - IssuerUrl = "https://localhost:8001" - SigningKey = "" - AdminDB = "host=db-admin;port=5432;username=username;password=root@321;database=EdFi_Admin;pooling=false" - SecurityDB = "host=db-admin;port=5432;username=username;password=root@321;database=EdFi_Security;pooling=false" - } - -.\build.ps1 -APIVersion "1.2.2" -Configuration Release -DockerEnvValues $p -Command BuildAndDeployToAdminApiDockerContainer diff --git a/BuildDockerDevelopment.ps1 b/BuildDockerDevelopment.ps1 index 24c0cd6fd..b5d902ab5 100644 --- a/BuildDockerDevelopment.ps1 +++ b/BuildDockerDevelopment.ps1 @@ -17,14 +17,13 @@ #> $p = @{ - ProductionApiUrl = "http://api" - AppStartup = "OnPrem" - ApiStartupType = "SharedInstance" - DatabaseEngine = "PostgreSql" - PathBase = "adminapi" - EncryptionKey = "" - AdminDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Admin;Application Name=EdFi.Ods.AdminApi;" - SecurityDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Security;Application Name=EdFi.Ods.AdminApi;" - } + Authority = "http://api" + IssuerUrl = "https://localhost:5001" + DatabaseEngine = "PostgreSql" + PathBase = "adminapi" + SigningKey = "" + AdminDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Admin;Application Name=EdFi.Ods.AdminApi;" + SecurityDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Security;Application Name=EdFi.Ods.AdminApi;" +} -.\build.ps1 -APIVersion "1.2.2" -Configuration Release -DockerEnvValues $p -Command BuildAndDeployToAdminApiDockerContainer +.\build.ps1 -APIVersion "2.2.1" -Configuration Release -DockerEnvValues $p -Command BuildAndDeployToAdminApiDockerContainer diff --git a/Docker/Compose/mssql/compose-build-binaries.yml b/Docker/Compose/mssql/compose-build-binaries.yml deleted file mode 100644 index d729837a4..000000000 --- a/Docker/Compose/mssql/compose-build-binaries.yml +++ /dev/null @@ -1,79 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Licensed to the Ed-Fi Alliance under one or more agreements. -# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -# See the LICENSE and NOTICES files in the project root for more information. - -version: "3.8" - -services: - nginx: - build: - context: ../../Settings/gateway/ - dockerfile: Dockerfile - environment: - ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" - ports: - - "443:443" - container_name: ed-fi-gateway-adminapi - restart: always - hostname: nginx - volumes: - - ../../Settings/ssl:/ssl/ - - ../../Settings/gateway/adminapi-packaged.conf:/etc/nginx/templates/default.conf.template - depends_on: - - adminapi - - adminapi: - build: - context: ../../ - dockerfile: api.mssql.Dockerfile - environment: - ADMIN_MSSQL_HOST: db-admin - PATH_BASE: "${ODS_VIRTUAL_NAME:-api}" - TPDM_ENABLED: "${TPDM_ENABLED:-true}" - SQLSERVER_ODS_DATASOURCE: db-admin - SQLSERVER_ADMIN_DATASOURCE: db-admin - SQLSERVER_SECURITY_DATASOURCE: db-admin - SQLSERVER_USER: ${SQLSERVER_USER:-edfi} - SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" - DATABASEENGINE: "SqlServer" - API_MODE: ${API_MODE} - AUTHORITY: ${AUTHORITY} - ISSUER_URL: ${ISSUER_URL} - SIGNING_KEY: ${SIGNING_KEY} - ADMIN_API_VIRTUAL_NAME: ${ADMIN_API_VIRTUAL_NAME:-adminapi} - ODS_API_VERSION: ${ODS_API_VERSION} - ENCRYPT_CONNECTION: "${ENCRYPT_CONNECTION:-false}" - depends_on: - - db-admin - restart: always - hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} - container_name: adminapi - healthcheck: - test: ${ADMIN_API_HEALTHCHECK_TEST} - start_period: "60s" - retries: 3 - - db-admin: - build: - context: ../../Settings/DB-Admin/mssql/ - dockerfile: Dockerfile - environment: - SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" - ACCEPT_EULA: "Y" - SQLSERVER_ADMIN_DATASOURCE: db-admin - SQLSERVER_SECURITY_DATASOURCE: db-admin - SQLSERVER_PORT: 1433 - SQLSERVER_USER: ${SQLSERVER_USER:-edfi} - SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" - ports: - - 1433:1433 - volumes: - - vol-db-admin-adminapi:/var/lib/mssql/data - restart: always - container_name: ed-fi-db-admin-adminapi - -volumes: - vol-db-admin-adminapi: - driver: local - name: vol-db-admin-adminapi diff --git a/Docker/Compose/pgsql/compose-build-binaries.yml b/Docker/Compose/pgsql/compose-build-binaries.yml deleted file mode 100644 index 1fb3dc8ca..000000000 --- a/Docker/Compose/pgsql/compose-build-binaries.yml +++ /dev/null @@ -1,71 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Licensed to the Ed-Fi Alliance under one or more agreements. -# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -# See the LICENSE and NOTICES files in the project root for more information. - -version: "3.8" - -services: - nginx: - build: - context: ../../Settings/gateway/ - dockerfile: Dockerfile - environment: - ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" - ports: - - "443:443" - - "80:80" - container_name: ed-fi-gateway-adminapi-packaged - restart: always - hostname: nginx - volumes: - - ../../Settings/ssl:/ssl/ - - ../../Settings/gateway/adminapi-packaged.conf:/etc/nginx/templates/default.conf.template - depends_on: - - adminapi - - adminapi: - build: - context: ../../ - dockerfile: api.pgsql.Dockerfile - environment: - ADMIN_POSTGRES_HOST: db-admin - POSTGRES_PORT: 5432 - POSTGRES_USER: "${POSTGRES_USER}" - POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" - DATABASEENGINE: "PostgreSql" - API_MODE: ${API_MODE} - AUTHORITY: ${AUTHORITY} - ISSUER_URL: ${ISSUER_URL} - SIGNING_KEY: ${SIGNING_KEY} - ADMIN_API_VIRTUAL_NAME: ${ADMIN_API_VIRTUAL_NAME:-adminapi} - ODS_API_VERSION: ${ODS_API_VERSION} - depends_on: - - db-admin - restart: always - hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} - container_name: adminapi-packaged - healthcheck: - test: ${ADMIN_API_HEALTHCHECK_TEST} - start_period: "60s" - retries: 3 - - db-admin: - build: - context: ../../Settings/DB-Admin/pgsql/ - dockerfile: Dockerfile - environment: - POSTGRES_USER: "${POSTGRES_USER}" - POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" - API_MODE: ${API_MODE} - ports: - - "5401:5432" - volumes: - - vol-db-admin-adminapi:/var/lib/postgresql/data - restart: always - container_name: ed-fi-db-admin-adminapi - -volumes: - vol-db-admin-adminapi: - driver: local - name: vol-db-admin-adminapi diff --git a/Docker/KeyCloak/realm-and-users.json b/Docker/KeyCloak/realm-and-users.json new file mode 100644 index 000000000..a34475f21 --- /dev/null +++ b/Docker/KeyCloak/realm-and-users.json @@ -0,0 +1,4233 @@ +[ { + "id" : "5d31328d-5800-4d03-8c09-2a5c3eb610b9", + "realm" : "master", + "displayName" : "Keycloak", + "displayNameHtml" : "
Keycloak
", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 60, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxTemporaryLockouts" : 0, + "bruteForceStrategy" : "MULTIPLE", + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "343f47e6-ca26-4581-a19f-54e0d5a91c29", + "name" : "admin", + "description" : "${role_admin}", + "composite" : true, + "composites" : { + "realm" : [ "create-realm" ], + "client" : { + "edfi-admin-console-realm" : [ "view-identity-providers", "query-realms", "manage-authorization", "view-clients", "query-clients", "manage-identity-providers", "create-client", "impersonation", "view-events", "manage-clients", "manage-events", "view-users", "query-groups", "view-realm", "view-authorization", "manage-realm", "query-users", "manage-users" ], + "master-realm" : [ "manage-realm", "view-identity-providers", "view-events", "query-realms", "view-clients", "view-realm", "manage-events", "manage-authorization", "view-users", "query-users", "manage-identity-providers", "view-authorization", "impersonation", "query-clients", "create-client", "manage-clients", "query-groups", "manage-users" ] + } + }, + "clientRole" : false, + "containerId" : "5d31328d-5800-4d03-8c09-2a5c3eb610b9", + "attributes" : { } + }, { + "id" : "a2a9aa6c-c085-44e5-8408-645b9253ef6b", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "5d31328d-5800-4d03-8c09-2a5c3eb610b9", + "attributes" : { } + }, { + "id" : "a30754a7-a2e9-48f5-9e43-20b084c62c30", + "name" : "default-roles-master", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "view-profile", "manage-account" ] + } + }, + "clientRole" : false, + "containerId" : "5d31328d-5800-4d03-8c09-2a5c3eb610b9", + "attributes" : { } + }, { + "id" : "e7941f28-3c51-4575-9ce9-7012b2fdcad2", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "5d31328d-5800-4d03-8c09-2a5c3eb610b9", + "attributes" : { } + }, { + "id" : "9e4535e4-746b-4d0f-b32c-186172ccc12b", + "name" : "create-realm", + "description" : "${role_create-realm}", + "composite" : false, + "clientRole" : false, + "containerId" : "5d31328d-5800-4d03-8c09-2a5c3eb610b9", + "attributes" : { } + } ], + "client" : { + "edfi-admin-console-realm" : [ { + "id" : "35bcb9c8-4ee9-412f-b21d-26043cca36dc", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "50aa1aae-ce12-4990-93ca-399a446e4156", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "011885c3-0255-4196-8b3f-11a78f1fdba9", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "94e0da74-e473-420f-8a5d-60fdcf59496e", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "119c5706-b277-4c61-a2fc-ceba7c4da287", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "edfi-admin-console-realm" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "0a7f9d03-865a-4cdc-9da4-19c8f890a1a8", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "194a36a0-10f5-4348-90f4-301223bb40ab", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "b4db236e-dfde-4884-85b8-d5bc580add15", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "58a28594-4b32-4892-980d-2dc9fc472efd", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "a77408e7-5366-45d1-8da7-e04825cae877", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "edfi-admin-console-realm" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "395e50b7-639f-4fb7-8b49-668c8048eac6", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "5c4ef008-b3a4-4b8d-917d-7f2d93027aab", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "ccd0de50-9203-48aa-9226-d88feb6cc0ce", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "21504df4-d3ca-4b78-b4b7-0d6dbf8519fe", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "e08b9554-3766-4d7f-9a7e-58bfdf632110", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "feb56580-733b-452d-8d9a-587c65528478", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "d245db0d-cf76-4452-a7f2-27cbc721c398", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + }, { + "id" : "ab4667e6-ec27-4023-bf1b-f88dac6ec311", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "4a4859ca-b83f-41b5-aa3b-afd92ef5d669", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "4eeaa5ff-f6a8-4a25-b745-77adf25fc143", + "attributes" : { } + } ], + "master-realm" : [ { + "id" : "a979d809-a9d6-4b13-91c3-299fb1c9d995", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "f1c3e9d3-e2e8-4e28-b23c-309e9d5e0dba", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "master-realm" : [ "query-users", "query-groups" ] + } + }, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "1c579b39-01a5-4865-8abe-46b96e2d0f97", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "11137aa4-238c-4fe1-a062-db6c11ab7026", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "acdb90ac-d90a-4130-a262-11d983131cc4", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "3370dc7b-3cb8-4146-9549-32ab5b05ebc1", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "03ceae1d-0580-48f3-84b9-2064f9ae4da6", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "c4dbe34e-92be-4e8b-9d3d-b0d808332889", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "c49c1dcd-8856-4cbf-a513-008c0f6a2097", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "767d5b62-397e-4c1b-8775-842ac5c0c609", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "a01e078f-fbce-4626-b39a-8f4ec8b07e97", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "b542a5fb-f051-4e3c-bead-1d88b9a647cc", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "master-realm" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "6a4c88cb-2158-4d69-9c18-acbae916e04a", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "e292c726-ccb3-4901-9494-bfac606d648c", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "cb3f8ebd-95a4-44d8-a22f-7db93c0a1af2", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "cd1e771c-0d88-427a-ad08-5a04f833017f", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "bad71457-1e06-452c-888b-df38a7ddc395", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + }, { + "id" : "035e20bb-0319-4cd3-b3fb-01a6c11a83d7", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "attributes" : { } + } ], + "account" : [ { + "id" : "89c6351b-b277-4e6f-94ec-d1ae4e9ccdf9", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "9b179baa-6b78-4c47-bbc2-af975373cde6", + "attributes" : { } + }, { + "id" : "8a850b6f-1e20-43ac-8a53-4fab90b7364c", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "9b179baa-6b78-4c47-bbc2-af975373cde6", + "attributes" : { } + }, { + "id" : "7e1468dd-b387-4293-9f9f-8dbb24e7e003", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "9b179baa-6b78-4c47-bbc2-af975373cde6", + "attributes" : { } + }, { + "id" : "5c3d1f06-0bd8-49f4-8488-cc4b5aa30de6", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "9b179baa-6b78-4c47-bbc2-af975373cde6", + "attributes" : { } + }, { + "id" : "f54616ed-13e7-41c4-b0c7-39ef5e1e8e64", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "9b179baa-6b78-4c47-bbc2-af975373cde6", + "attributes" : { } + }, { + "id" : "570cb2db-ec31-48c3-b488-465dab26696b", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "9b179baa-6b78-4c47-bbc2-af975373cde6", + "attributes" : { } + }, { + "id" : "a76a92a1-9354-4cb8-ad58-85c8cbd732fb", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "9b179baa-6b78-4c47-bbc2-af975373cde6", + "attributes" : { } + }, { + "id" : "a1ee2fed-a035-42d7-a7a3-eef5dfb8676b", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "9b179baa-6b78-4c47-bbc2-af975373cde6", + "attributes" : { } + } ] + } + }, + "groups" : [ ], + "defaultRole" : { + "id" : "a30754a7-a2e9-48f5-9e43-20b084c62c30", + "name" : "default-roles-master", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "5d31328d-5800-4d03-8c09-2a5c3eb610b9" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], + "localizationTexts" : { }, + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256", "RS256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyExtraOrigins" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256", "RS256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessExtraOrigins" : [ ], + "users" : [ { + "id" : "8b90051c-e2fe-4dbd-9770-3aa37eabb334", + "username" : "admin", + "emailVerified" : false, + "createdTimestamp" : 1733170004521, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "308a7f18-274d-48d1-abab-a8c5813aa61c", + "type" : "password", + "createdDate" : 1733170005084, + "secretData" : "{\"value\":\"LR4Cksg/IR7swCmtolZuvanvLzw8egHgn49ayps/I+0=\",\"salt\":\"wL2mzmFO139rLdDiFTIfMA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "admin", "default-roles-master" ], + "notBefore" : 0, + "groups" : [ ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "9b179baa-6b78-4c47-bbc2-af975373cde6", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/master/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/master/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "78faa61e-ee6d-402f-8acc-a4656fd0f766", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/master/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/master/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "3e611b5e-9ac2-43e0-9700-4e17456fd005", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "bda0f7d4-ca78-4a19-9465-eaf47bdbcb6c", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "client.use.lightweight.access.token.enabled" : "true", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "4eeaa5ff-f6a8-4a25-b745-77adf25fc143", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "true", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "36d7d2dd-1d86-4c0b-beb1-083a4f24afcc", + "clientId" : "edfi-admin-console-realm", + "name" : "Ed-Fi Admin App Realm", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "true", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ ], + "optionalClientScopes" : [ ] + }, { + "id" : "85047c38-9de2-4b2a-ae75-86a18c039701", + "clientId" : "master-realm", + "name" : "master Realm", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "true", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "ad224f93-2679-4c78-a4f6-77120786fb70", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/master/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/master/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "client.use.lightweight.access.token.enabled" : "true", + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "d4e9fc66-88bc-4483-9576-b3bd52556851", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "42bc4082-079a-46da-848b-10ca53e7fec5", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "consent.screen.text" : "${rolesScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "47efb4ab-3bca-4fa0-913f-8dba5eda29a6", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "74b975f4-52b5-4700-ad24-d4f2bfd2c210", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + }, { + "id" : "bbc5b49a-5911-4ace-9d69-93b50f68de17", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + } ] + }, { + "id" : "4d04b15e-71c5-4630-871e-21443460bc33", + "name" : "basic", + "description" : "OpenID Connect scope for add all basic claims to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "6c87032a-cb8c-42de-95d9-c0f6054da04b", + "name" : "auth_time", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "AUTH_TIME", + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "auth_time", + "jsonType.label" : "long" + } + }, { + "id" : "e47ad225-5ef2-4422-bf42-1520b9a01629", + "name" : "sub", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-sub-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "ae4a50f2-3eba-4eaf-94a8-fd2a462d8f74", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "7bc3d3ea-699b-49aa-9b47-774b0136bd38", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${profileScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "42e2da5e-ed5b-400f-bf58-ba70a3a66804", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "e9f7d478-dd0c-4477-ae1b-58f33445b979", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "c070c920-a388-40e8-acd3-f0a700e8f709", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "b1ccb299-1f11-43d0-b0f1-94aa27268cd9", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + }, { + "id" : "ec75dfde-9636-4a1b-9539-7f77d249ec99", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "18bfb81c-17cc-4cf8-9da8-7ad0416eca54", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "845a8dba-5968-4087-bb02-4b0722344201", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "47ce4908-66b4-4a91-a75f-b4a400592183", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "3ef54cce-990f-4995-b50c-66ac8ef611f2", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "acbdf281-807f-43c0-a075-b22d4c871888", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "1f9c169f-0e26-4857-87a6-2cb47a4f468f", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "8af91569-c869-403e-ab7b-3a2671a63754", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "9b6538c8-3cfe-4c62-add6-a438f7d8cfc5", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "2620f651-20ca-42aa-bdee-158c243ffa0a", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + }, { + "id" : "3c49afaa-5ba9-4228-8d75-18ab3fed0714", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "eabd9454-cae2-4185-b77d-558ececa3bf1", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + }, { + "id" : "f15a705f-949c-4490-a83f-9b58cb3b3325", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${emailScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "6f4fba01-23e7-437d-a15b-61712a200857", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "b6a3e069-6330-4465-8ca4-cebd70d02518", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "da56c2ca-db7b-47ca-8e41-37d89e50adc8", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${phoneScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "41a942e8-add4-4b2f-9529-a91c872d8d32", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "9172295f-9f94-4a87-8975-4b987aa9d46d", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "b051e380-0f37-4805-9f7c-577086786838", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "9b7a8b1e-d5c0-4380-9c89-4aac6cb11835", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "240c1a20-7e98-49ad-b837-5dc9cf5b8d93", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${addressScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "ef893c51-8225-4b45-a430-89642fc3ab71", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "introspection.token.claim" : "true", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "e5276dce-8c6e-4992-9eae-ffefe3513398", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "consent.screen.text" : "", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "3b216687-a1ac-466c-89f8-dc50903bcae0", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "274a00c0-8756-4768-baa3-2cd74f04883e", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "2b3b9f08-f8a6-48a8-86d3-61a19926ce92", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + }, { + "id" : "9e3c438e-69d7-4fd3-a53e-989b19ddb3ca", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr", "basic" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "referrerPolicy" : "no-referrer", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "ab2118e7-454a-424c-8558-abe113246b1c", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper" ] + } + }, { + "id" : "65541fe2-a21f-4e1a-bc8a-df2a84e10d60", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "0e29bfbd-a01c-4032-8471-cdd0237b4ba5", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "2b399def-c646-4e90-9cb2-c21d7707bcb3", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "6d4a8d66-c4b3-4a4a-95c8-7735918e5c76", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "4068c2c0-6951-454c-ae3a-340b0ef3994d", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper" ] + } + }, { + "id" : "71c0a1b9-583f-4e2e-b2f3-88f5125aab72", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "dc7edafa-6541-4f02-b099-f1d868313828", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + } ], + "org.keycloak.userprofile.UserProfileProvider" : [ { + "id" : "11182b4e-b577-4e58-aa51-410f38eb7a18", + "providerId" : "declarative-user-profile", + "subComponents" : { }, + "config" : { + "kc.user.profile.config" : [ "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}],\"unmanagedAttributePolicy\":\"ENABLED\"}" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "e43e47ba-ddcd-472c-b701-24fc5ca11539", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "bdeb130e-2e15-4e56-9a6b-0b5ceee73069" ], + "secret" : [ "dhzKjL1E1ghmlC15C3aojg" ], + "priority" : [ "100" ] + } + }, { + "id" : "41a21c75-c26f-4e2a-9907-7b94249a6f19", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEA0VPKItovNPstigLK/bIzXQTyIDu8rr85YrbNubWK5BtQPZdz62doN97Orhnc3ly6Yu7qBDCRZUBF6idw44+WSf8qM+DL5JufLitjVQnHSdtR+bd+HFAF6NHPdiCkKtBQEdwIzLe/KtbXxo+eBszK47XMED/IoxLGyVZtFXtf9WW4I+9t/TLCbuUKot1ii09JevfxiS5r+Hm2BLncMKJzYhtmghworsKn6YGA248MIiV9T+JBqxUUoc3Cy67kHuPBZp7Xr9Xg6CpgEKcMVZpcRpvseT7oz209K7qXWyaGug8jlD72KXzqoSCpRXtOmvZtu3eDhOEIhgjc64WSqCvd0wIDAQABAoIBAAxJldj747+Bawfp6A6CUl+IdcoMl6POV8kCoArhrDh9nfUvmb9cv5JgAtPXcs6etNYv3g7G5xIBdtkq3YaOHC1VzB1lebJ+rBZit577RvZ/6Ez3R3DUBbTH9BykB973gWu69c1Gj/Msz1e5gHY3uUUqm8Lsa5Oe+4pAB4E2T8iwx68pOdAx9b98PfvNvZcmg3A+8wEclu4AIG7Ttis8WNJ0oyWfXqms3uWTFu+FpfsWyuo6OcHlPeWYLL9zxgZfRaOrHm7G9mtpIBVx+TfHUzSGZbXrgAoydhFWsGY1X8Z//g4NLnKSGAE/iHsozVpS5XIAwre3ogSv3Xal5L4QkZkCgYEA8EcGIcfQKPChbLJ9ron18MI1LddM37oDE7W9Y4nP2jir/vcg1YVkKXMnCo/eUY0fxFVNt38q0+OVhpFK+Oub1QQGikEjCGpJliV7LJWXzZtaS6MXgXZSMuBO7kp/0EyvuSMuGzYReTbM5USznWkilA1bIij0xV5LRywVGJJFM/8CgYEA3wZO+w/NV5VsLayEE8Vzhrh4BHB+dYNEfxvvseJYru9xj3PsMzeB/bvk6LaF+Sb4re5Tl6wU2xKYK7/pgpTREpYM8yMksRJwBytAMVnkrfSMm/UmaH/eRETZRy8Cz/0xVFh+sD7Hke7qk0/zPst21dPDB0qZCBu0ICDFfvI2Ri0CgYEAsH+rnHLXkMs5+MugkO6cr68pOzQ1Lv7HtdnBdfXMrgMFLPRBtTcYHW6OFi8aYxNka2cyAIG0ZxN9Se0+gl0oCjC70SEJ5+uThGyh710vtoPQr1DtOlc9CExHDrnA67T5O+kAvFJaQY0ks6Fkw1NwaArrj8tACh868twJ3xPLK+cCgYAmbHTRBmDvVPwFRnKe4WbM8B0lTqhK4Km9aHe8Yu26feBHwcYQ8OBBccy3VzLvwQoZFTVrrScOsz5qUy8M40y98gIbJZPdufp5EbeGiSE/4yESgYLzgAwwINh6Thwi71E/Ydu5y6cgelQJuz2CNRWtM0WnjO+emP9Ihbzg6SP2pQKBgAvUxhuMK6GHb2lhIgeO6Vh7REho1DeBvHj6OdwW/udI3wpPbaSG9FJuujdpVw2SeptNag61A9LY/oZ8y7Qt9oj4xrdKPAnqpwYmNnCQTHcxW+Eo5Uuj8gzpmhWnkaaEigVpLzRUubEfbYQIh3qjAKREK7SIP8U+gMapCA1j8mKa" ], + "keyUse" : [ "SIG" ], + "certificate" : [ "MIICmzCCAYMCBgGTiPt5OzANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQxMjAyMjAwNTAyWhcNMzQxMjAyMjAwNjQyWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDRU8oi2i80+y2KAsr9sjNdBPIgO7yuvzlits25tYrkG1A9l3PrZ2g33s6uGdzeXLpi7uoEMJFlQEXqJ3Djj5ZJ/yoz4Mvkm58uK2NVCcdJ21H5t34cUAXo0c92IKQq0FAR3AjMt78q1tfGj54GzMrjtcwQP8ijEsbJVm0Ve1/1Zbgj7239MsJu5Qqi3WKLT0l69/GJLmv4ebYEudwwonNiG2aCHCiuwqfpgYDbjwwiJX1P4kGrFRShzcLLruQe48Fmntev1eDoKmAQpwxVmlxGm+x5PujPbT0rupdbJoa6DyOUPvYpfOqhIKlFe06a9m27d4OE4QiGCNzrhZKoK93TAgMBAAEwDQYJKoZIhvcNAQELBQADggEBALQxHVCn5pF54RKol6QJzxj+JSRRubhjF7TkknvzZe/YF0KT0EHX7OSHoleSu8Xan/4O7GMrCjU96u8sy7vzRJgDMRHIL3cQzFm4+p+faUqer8PxsnMI/B9QtWNYdJcfFp+UUUdlI0ChEU2NBIede/9eIr5jlEf+2MLl73dn7tjpugwqoH9db4ovDK6wRJyaR6xr1lJumqRWcqS4uU85nIAAvrfHESQYrYhh+AZnwtObs4Lwt65v9RlekoIv3B6z4mnfK56gpQoaTsuImPLr/9aKHmI1MuYUcLpj8N3c92TL1JyJ2ZPQkpIwML3aq93P/nt7TS3L+CwGtuDp8SdA9fw=" ], + "priority" : [ "100" ] + } + }, { + "id" : "88b6415f-5c5f-49cb-a146-8fb3c0c5f557", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAq3kiGuBAyDVCJbhZFbRt2BNSlDqHjnJxbHBIDq7nIVMP/Ekc9MSmR2Myjx6/DPHlzUOigp85HANmvts2EunZ/TH7QzCtIJ0jP9dsQrwXsgUFjASf7T49njOFFAmz/xtOLInk3zQ2dNK2EJsNHxTngwETv0sjlIbJIRRjhJuGuOs3GlTFM6qjauCKx9zzYGW6kGuqpdhgAm3MydQZMH/KwUBOe7C3Bz3YSPsFnDs9Io1SLhxkwNsc4AKds/v1F5c2MvdbUAno+3a+CMRBY14FHCTcbxUqMdo/5rzHosQb7fJSycogukzEo9YaAgnaW1uBcRIGcE2FkBR47w+qbF70vQIDAQABAoIBACcEKP50iEkF1Uc/6wVFSLkhrGiN6MpAwA4sfjjZEz5B6CE/gZcwJ6WWrKpkFJSddex+tzuBfz0giQBkYKltDGzQMpvXN410M9ES8087/ShEfD6fzsPspZiZoG4uxwYE8ISx26Ht/3YVe9fLb/DikMr+M8ATGMVy5dDhEsPZn43zl/0zlvOrW0cg04NhDsxZZvu3oD0sRmp9WxoAstUs0yyYnb4cQXKF47I32YyQYWGxdnvI0LGTFG8MT6PmxMlMG1tamk9fLQRur0UpEnzrMKojWctLsTyQcRi8XZBlLC9s/3s5NNtgxkOZFuUbPfKvStaFfySm7N0RBSg9zWy8IAECgYEA0obS1X5ZE6oBmv/7vhI20+vq0WmP6ZNPABJulXJmRbsBVNmUMcZ7Kk3sfYlbfMTtTuK+oL9DacPqKo2DTh/bbl4svaJYFTHYdIuMZU51Y6QN0EsWgwT0bZNONnU5uXdejUL7QM6LOOwOPilJpCPh4Gl4e8uEYIyiI62xNbW9V10CgYEA0ILT/mqRB769hJWI/8mlx4QdfAmHBxLDgrOfhqnHha/Lbs4ptnD6VR7XZNbS4HerrwP2RcAnhx/tn16rl9v5xydeBVXo0y0gJXC254PrJPLNXb5FB7hnyNRMrG04CjZraaSZT6LeoiqbdJlDdJquaOVCXxIA4NMjvKZmUwWTHOECgYA7izaGe73l+eeKV2x0TeKD/e3kXN7/SVLGym/MhRa4EjRGM2Pe0Cx1wqRovTjLCUe1UBxqiq4ec/L/JhdFOW1uweQVnp0bIYX/MJj87jv1eqkNiK9NT1FyzH8KmA1Dj6eGfLNjmQd3igxdOIBDlIKGXk5ddSpzxFL3lGDSuBxdYQKBgAr5+CL6hSv9NIjYPmAIw3tfTxYKglsy96vXvNwf7+rvuCvbGoMKK4Nzni8mx+8dHaKW+M3vMaWBXsb2+t2BrQJ45hAt15XWWEzgRCCn5ycIyRrYO1YOtRfzZR3mmf6fosPq8E8q6cFrsMdQkiZ/RhMrjnTmw7kSJF13/Qb90iLBAoGBAL7F84OWkuRzZJedDiOezzT+gX7xINIGiR42BgaB0z2kav6hdBDdQv6u1ozOivGF1AmSOaj+/MkTc8jY9qOsRJO5hU+EMqaKmtfha7aD/f8j1hpe/HcAXWvcC0VKiL8YLolaYEhzgsAif+MugFrj2j572N9Yn2N44hecuKk1JrHd" ], + "keyUse" : [ "ENC" ], + "certificate" : [ "MIICmzCCAYMCBgGTiPt68TANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQxMjAyMjAwNTAyWhcNMzQxMjAyMjAwNjQyWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCreSIa4EDINUIluFkVtG3YE1KUOoeOcnFscEgOruchUw/8SRz0xKZHYzKPHr8M8eXNQ6KCnzkcA2a+2zYS6dn9MftDMK0gnSM/12xCvBeyBQWMBJ/tPj2eM4UUCbP/G04sieTfNDZ00rYQmw0fFOeDARO/SyOUhskhFGOEm4a46zcaVMUzqqNq4IrH3PNgZbqQa6ql2GACbczJ1Bkwf8rBQE57sLcHPdhI+wWcOz0ijVIuHGTA2xzgAp2z+/UXlzYy91tQCej7dr4IxEFjXgUcJNxvFSox2j/mvMeixBvt8lLJyiC6TMSj1hoCCdpbW4FxEgZwTYWQFHjvD6psXvS9AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHJH1uAXRq4rGNdbsa+A/87WZ/GDksZk24HKeLTrXVh7pNIVHq6z7JTchNnUEDOYiAgcpGNQ/hJDam2TFJL36gkm1+kb29rzMydMueyDQxUyt68oTgE+fBqJYpEZLctF6NDRHD+cF+65i+PZ4jNmy7vHNYRRDvb946bTENqmkf4Gu5LCeAeqO/Hry5yX9X0l0y4Cy5iklipf1lffU9Hxd5hHQfncWEBPxY2FoiaIS4Zdo/V9iF7Idg+MWBqr/HThF3Yh0q8uupK0TMZZWbAKvUsn3tYKRu2zCQboH0+dLq2WRpbP35JhckSpG/GfdN2vGpny1hkSVtV2GFq4L+VEaoo=" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + }, { + "id" : "7d134fa1-564a-4e7a-ad2c-7cd143826c76", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "0c75f580-34a5-47f4-9fae-a63d72a4edcd" ], + "secret" : [ "2jHRcN7TFNWpDv6U_6zK0c7wm9qyqvE-aujzmupFGrq3wNNzQv24sj1WBLVvQPzezNBGiWtRcFxidajiW2KaOMjZn3GLtWHdDKRlidOcHjCitkIbOoPezXd8lVTdaX6aHTzC-jBg3oZBVLDiDEPUeTzJrI79PJs0csoAQ3rjbmc" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + }, { + "id" : "7a115e60-3af5-4676-b219-374a25ff8570", + "name" : "hmac-generated-hs512", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "3fa23313-9eb5-4d59-9011-aabeeec9e1b3" ], + "secret" : [ "SCR1qxE_vLMrZ-pvQmlFnZl6RAL14SzD84ggu5Fg9_BJe6wr8p-NHWlkArw3o0VpK5N8eYkOTsH_ZFpxn4rwrGX5p4zCksafB3hsbPy_6gliL7yg9OPhEjh9ShSeTuxp8EuPq_LxTpmT8ginlgPNXFzihNzQ0CXB7PZ-z-ilJ2g" ], + "priority" : [ "100" ], + "algorithm" : [ "HS512" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "6c80b500-9c8c-40d8-81f5-2772a57d2be4", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "d03c21a3-1788-4033-920c-5b651ccdafca", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "9f7adaa3-e938-4f12-80cc-1d886c2b2b61", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "d60d801b-10d0-4783-a081-8c4d2fb21169", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "e3a946f2-d9c3-4ed9-b9f8-46e93cb29321", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "31c27f94-254a-4cc2-ae6e-8d60a448d690", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "583be0c0-7b46-4290-addb-9efbd0a7d455", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "3c338291-f939-4424-a381-6101a5ba6559", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "1ed25189-00dc-447d-9832-260cefb704ee", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "64869c33-05f4-4c0c-be1c-f99201fc208c", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "0231cd45-7961-4b05-a33e-e40b9c9ea899", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "a0d856aa-d1d7-493b-be46-b6044388c5cf", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "afe04109-aa95-4b5c-8b1d-21be5dd5d2f4", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "913577ea-c6e5-4c86-8a2b-a01cdcca8421", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "ef262c93-7529-411e-a67c-a413cd20ab4e", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "0bf59a92-c2cb-4711-8371-f6bea74445b6", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-terms-and-conditions", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 70, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "d677ee20-e507-4bbf-9d82-d8f2d3b50683", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "3d083d47-e4c0-4779-af6a-3dd9f8194f10", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "b6c15c63-288f-4b4c-a5c8-8fbf66da935f", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "cd63e771-4fd3-4942-bb7e-c5cc10c5506f", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "delete_credential", + "name" : "Delete Credential", + "providerId" : "delete_credential", + "enabled" : true, + "defaultAction" : false, + "priority" : 100, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "firstBrokerLoginFlow" : "first broker login", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "clientOfflineSessionIdleTimeout" : "0", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false" + }, + "keycloakVersion" : "26.0.8", + "userManagedAccessAllowed" : false, + "organizationsEnabled" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +}, { + "id" : "debc13fa-cc4a-48a4-adc7-64790f086ba8", + "realm" : "edfi-admin-console", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxTemporaryLockouts" : 0, + "bruteForceStrategy" : "MULTIPLE", + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "091347b2-568f-49f7-93a9-4f79b1d0ef51", + "name" : "adminapi-client", + "description" : "Role for AdminAPI users", + "composite" : false, + "clientRole" : false, + "containerId" : "debc13fa-cc4a-48a4-adc7-64790f086ba8", + "attributes" : { } + }, { + "id" : "4df7ceb1-8013-404e-a3be-b1549b1cefbe", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "debc13fa-cc4a-48a4-adc7-64790f086ba8", + "attributes" : { } + }, { + "id" : "299c058e-116f-4259-96d8-83ba720b3411", + "name" : "default-roles-edfi-admin-console", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "manage-account", "view-profile" ] + } + }, + "clientRole" : false, + "containerId" : "debc13fa-cc4a-48a4-adc7-64790f086ba8", + "attributes" : { } + }, { + "id" : "ac0f777c-8605-4bb7-99cc-6534cd44285f", + "name" : "adminconsole-user", + "description" : "Admin App User Role", + "composite" : true, + "composites" : { + "realm" : [ "adminapi-client" ] + }, + "clientRole" : false, + "containerId" : "debc13fa-cc4a-48a4-adc7-64790f086ba8", + "attributes" : { } + }, { + "id" : "91d0a7da-fcf5-4a4d-996b-9049c7524a15", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "debc13fa-cc4a-48a4-adc7-64790f086ba8", + "attributes" : { } + } ], + "client" : { + "adminconsole-worker-client" : [ ], + "realm-management" : [ { + "id" : "9cd73fd8-d11d-4710-98b1-8d1b26547ca6", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "6955de36-5365-43c4-9fb0-3f6b9d97baf8", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "06e2e1a2-e404-47ae-9b86-a6ec39a1643d", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "7b0d057d-48c8-4895-b6fc-92ee25bb28d8", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "c813ce66-9520-4807-8c13-4547918923bf", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "3ed2f7f4-6dc2-486f-a0c7-5042464ac168", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "9cdd3480-62dc-4fe8-b058-bee4893ad19b", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "f7965c95-4491-448d-a05f-d77af723e495", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "5141373e-a181-47a3-a3f8-f912f1f8f9aa", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "89441521-dc71-4561-96a4-fdb9fce33720", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "86145a1e-c0d8-4433-8e64-501d89c2ecb5", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "view-identity-providers", "manage-identity-providers", "manage-clients", "create-client", "view-clients", "impersonation", "view-users", "query-realms", "query-clients", "manage-users", "view-realm", "manage-events", "manage-realm", "query-groups", "view-authorization", "manage-authorization", "view-events", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "f34b7003-4c25-4917-bb48-a2ee877a52ef", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "490a9208-0e8b-4a30-a20b-fb8ab762866a", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "283625c7-6545-457b-8511-37eb245fb9b2", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "89ec6a42-a019-4755-ab6f-09a301521f4e", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "e2c81fc1-1583-4600-834c-9ddeea0f8337", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "a10f85d2-dbc7-460e-85ed-1c9d077a80bb", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "c5c976a1-c0c6-42b7-b5cf-25b6029150c2", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + }, { + "id" : "000adf7f-d6c4-4655-8b94-88d2853bdd70", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "attributes" : { } + } ], + "admin-console" : [ ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "05f26654-83b2-4828-a7c9-40b992a91526", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "e529f56a-9649-4159-a7d3-5c0e6fe8bde1", + "attributes" : { } + } ], + "account" : [ { + "id" : "b70f3f9c-a888-458e-8c9b-d256f2367c66", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "08c22c00-7900-44c1-9ac5-979ad89b084e", + "attributes" : { } + }, { + "id" : "8a5773f6-8f7d-4e71-a670-42a923432862", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "08c22c00-7900-44c1-9ac5-979ad89b084e", + "attributes" : { } + }, { + "id" : "4b5e4bec-6a91-4fbf-9fa5-a3570cac4c14", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "08c22c00-7900-44c1-9ac5-979ad89b084e", + "attributes" : { } + }, { + "id" : "17aae21c-edc1-4e27-b07e-f7e092b334d7", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "08c22c00-7900-44c1-9ac5-979ad89b084e", + "attributes" : { } + }, { + "id" : "1f825d49-d8db-433c-8c55-8f7fd724e43e", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "08c22c00-7900-44c1-9ac5-979ad89b084e", + "attributes" : { } + }, { + "id" : "a9b6b14d-d0f7-4eee-b496-842e8cfef269", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "08c22c00-7900-44c1-9ac5-979ad89b084e", + "attributes" : { } + }, { + "id" : "05272454-81de-45ce-9d72-2caa91b241a0", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "08c22c00-7900-44c1-9ac5-979ad89b084e", + "attributes" : { } + }, { + "id" : "350b6f17-ff6f-48f1-9093-a1d74f5b7c8d", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "08c22c00-7900-44c1-9ac5-979ad89b084e", + "attributes" : { } + } ] + } + }, + "groups" : [ ], + "defaultRole" : { + "id" : "299c058e-116f-4259-96d8-83ba720b3411", + "name" : "default-roles-edfi-admin-console", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "debc13fa-cc4a-48a4-adc7-64790f086ba8" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], + "localizationTexts" : { }, + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyExtraOrigins" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessExtraOrigins" : [ ], + "users" : [ { + "id" : "4e0670af-8bc7-470b-b09f-d968e7aa78cd", + "username" : "adminapi-user", + "firstName" : "AdminAPI", + "lastName" : "Regular User", + "emailVerified" : false, + "createdTimestamp" : 1741550273160, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "424851d7-956b-4633-82d9-d35e11e67710", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1741550306399, + "secretData" : "{\"value\":\"g4ZywmCdOm30MX0+F0pS5BhkiAHN6Or5eWv2C4b5EdQ=\",\"salt\":\"j693Wenuw7El3Z/FKaqiFQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "adminapi-client", "default-roles-edfi-admin-console" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "afb00179-8182-468c-85c6-d6267f33f719", + "username" : "adminconsole-user", + "firstName" : "AdminConsole", + "lastName" : "Regular User", + "emailVerified" : false, + "createdTimestamp" : 1741492501533, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "5a1742cd-1ae3-4575-bccd-e92a76a12a34", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1741492518963, + "secretData" : "{\"value\":\"nay4VLKX5b0/7ZDsfc1eXkhtIPCMYBm35IK0cvFw5Q4=\",\"salt\":\"oCFgeJN2gw6Lx6Fj9BFPWQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-edfi-admin-console", "adminconsole-user" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "77c5e18f-6987-41c3-ae9f-b6c9135d8479", + "username" : "myuser", + "firstName" : "Admin App", + "lastName" : "TestUser", + "emailVerified" : false, + "createdTimestamp" : 1728914120740, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "6b65b312-5a34-443d-94dc-4a100604eb84", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1734464595279, + "secretData" : "{\"value\":\"Wesi0Gq/rYCKcNxQleoddBSAXIKY54nIo53qzsntoYk=\",\"salt\":\"RdEp32ADELJCPlTjQChf3A==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-edfi-admin-console" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "3f933c10-ae2d-4737-bb61-de8c02f94ec9", + "username" : "service-account-adminconsole-worker-client", + "emailVerified" : false, + "createdTimestamp" : 1741492695989, + "enabled" : true, + "totp" : false, + "serviceAccountClientId" : "adminconsole-worker-client", + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-edfi-admin-console", "adminconsole-user" ], + "notBefore" : 0, + "groups" : [ ] + } ], + "scopeMappings" : [ { + "clientScope" : "edfi_admin_api/full_access", + "roles" : [ "adminapi-client" ] + }, { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + }, { + "clientScope" : "edfiadmin/full-access", + "roles" : [ "adminapi-client" ] + }, { + "clientScope" : "openid", + "roles" : [ "default-roles-edfi-admin-console" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "08c22c00-7900-44c1-9ac5-979ad89b084e", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/edfi-admin-console/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/edfi-admin-console/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "ac159a7c-a83c-4d32-b6ba-b7f6672a8ee5", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/edfi-admin-console/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/edfi-admin-console/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "b16eba4d-a35d-423e-80b8-eb8f2c75b982", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "f34766c2-61c9-4054-af53-00ce8c7410c4", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "client.use.lightweight.access.token.enabled" : "true", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "7ad9909d-cf22-49c2-a29c-e496687fcd84", + "clientId" : "admin-console", + "name" : "Ed-Fi Admin App Web Client", + "description" : "Ed-Fi Admin App Client Web Site", + "rootUrl" : "http://localhost:8598/", + "adminUrl" : "http://localhost:8598/", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "http://localhost:8598/callback" ], + "webOrigins" : [ "http://localhost:8598/", "*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "*", + "display.on.consent.screen" : "false", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "e11b27a3-cc28-4e5f-9edf-f2c90f17cc74", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "userinfo.token.claim" : "false", + "user.attribute" : "foo", + "id.token.claim" : "true", + "lightweight.claim" : "false", + "access.token.claim" : "true", + "claim.name" : "http://schemas\\.microsoft\\.com/ws/2008/06/identity/claims/role", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "openid", "profile", "roles", "edfi_admin_api/full_access", "edfiadmin/full-access", "basic", "email" ], + "optionalClientScopes" : [ "address", "edfi_admin_api/worker", "phone", "offline_access", "microprofile-jwt", "edfi_admin_api/tenant_access" ] + }, { + "id" : "cf9b401b-2908-43e6-a48d-0bc9e7ed3176", + "clientId" : "adminconsole-worker-client", + "name" : "Admin App Worker Processes", + "description" : "Client for Ed-Fi Admin App Worker Processes", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "7tpYh5eZtL0ct99cmfCXUY3q5o2KxUTU", + "redirectUris" : [ "/*" ], + "webOrigins" : [ "/*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : true, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "oidc.ciba.grant.enabled" : "false", + "client.secret.creation.time" : "1741492695", + "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "+", + "display.on.consent.screen" : "false", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "9ea8659d-117a-4701-8998-dd486a03c43d", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "userinfo.token.claim" : "false", + "user.attribute" : "foo", + "id.token.claim" : "true", + "lightweight.claim" : "false", + "access.token.claim" : "true", + "claim.name" : "http://schemas\\.microsoft\\.com/ws/2008/06/identity/claims/role", + "jsonType.label" : "String" + } + }, { + "id" : "4ace2f5c-620e-4046-9a3c-8b1d805ef9a3", + "name" : "Client IP Address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientAddress", + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientAddress", + "jsonType.label" : "String" + } + }, { + "id" : "bbe36f40-2e49-4894-855a-cbcf135ec585", + "name" : "Client Host", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientHost", + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientHost", + "jsonType.label" : "String" + } + }, { + "id" : "55eb36d9-d37e-49f6-aab6-70f5a3f0645e", + "name" : "Client ID", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "client_id", + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "client_id", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "edfi_admin_api/worker", "openid", "profile", "roles", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "edfi_admin_api/full_access", "microprofile-jwt", "edfiadmin/full-access" ] + }, { + "id" : "e529f56a-9649-4159-a7d3-5c0e6fe8bde1", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "true", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "c877679f-d2b1-439e-bc6a-90654b73b3dd", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "true", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "d97bcd7f-22f3-470b-83ff-dc1bf4ab5b51", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/edfi-admin-console/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/edfi-admin-console/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "client.use.lightweight.access.token.enabled" : "true", + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "286bd54b-d8ec-418b-9f51-b5f458515c08", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "ff915e21-25d7-42af-9876-cd6fd4106930", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "64b7380e-ff4e-459e-b588-cbe1b0f04e02", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "42db2eb7-2dee-4898-831c-2551b9e2470b", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "783ad193-6e07-4d69-b280-e60e361a95e0", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + }, { + "id" : "70aaea8d-b950-4e0c-8e84-2a4902c2825e", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "deac865d-fc1c-470c-85db-802748ad2c67", + "name" : "edfi_admin_api/full_access", + "description" : "edfi_admin_api/full_access", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false", + "gui.order" : "", + "consent.screen.text" : "" + } + }, { + "id" : "46883dfa-9a63-4d02-90d2-e972cea69a27", + "name" : "edfi_admin_api/worker", + "description" : "", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "gui.order" : "", + "consent.screen.text" : "" + } + }, { + "id" : "60de541b-be5e-411b-9173-4a3de9bccea2", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${profileScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "7052aadc-3580-4926-be93-aa076bb2eb31", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "5afbee26-c385-41f8-b262-1e8c4a07b7d7", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "ca478946-33f5-4cfb-9be1-d576fe63868a", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "58065198-809e-4eff-84b5-bb9804c890dc", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "ead56ed3-cb13-46ee-9039-d3047de85f99", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "ec8773b2-840d-4458-8ba0-47208003b776", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "21812c29-9acb-4bcc-b913-c510496dbbe2", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "2e5e68d4-7072-4954-86ed-277998d56d3b", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "4a5a16ba-16f6-4580-a833-8e6ad2f815fa", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "0753eacf-6a07-451f-a1be-b04889954bea", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "aaab2ff8-6a9f-4713-9937-4fa00f546568", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "623c6735-6265-4d4e-9383-074d6b51abcd", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "96fa7661-e887-4a66-b280-8c20a1d34e41", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "1936fb1d-82ad-487a-a7b1-ef4c66737947", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + } ] + }, { + "id" : "432a0300-16b7-42ad-a87c-4d36626f102f", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${addressScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "81185ef8-a6dd-4b61-8e22-761606bdbef2", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "introspection.token.claim" : "true", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "208d8414-bdd8-4112-ac14-9d5aa8afaff3", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "3f6ab473-0803-4415-a1df-34cab781f53a", + "name" : "edfi_admin_api/tenant_access", + "description" : "", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "gui.order" : "", + "consent.screen.text" : "" + } + }, { + "id" : "128c0d70-091d-4b4c-9c17-e1957361d749", + "name" : "basic", + "description" : "OpenID Connect scope for add all basic claims to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "0bf6d668-0af2-48be-97f2-ac424be23498", + "name" : "auth_time", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "AUTH_TIME", + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "auth_time", + "jsonType.label" : "long" + } + }, { + "id" : "3705ba60-e4bd-4ce2-91cf-feeb9717479b", + "name" : "sub", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-sub-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "33621ec9-698f-4916-994c-bf42aaf6fdd6", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${emailScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "32166696-5142-41f4-ae38-1cd248bc87d7", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "f067d153-6826-4f8c-a5b0-f156da745dd8", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "614976a9-fd83-432f-bfef-e044d99e9d32", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${phoneScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "2a47ea2a-a579-4846-8228-71dacef7489f", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "fdb01de8-a9bd-4b61-aa76-54e131c326e7", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "bc0101c8-3d9e-4569-ba15-3786c7eed809", + "name" : "edfiadmin/full-access", + "description" : "edfi_admin_api/full_access", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false", + "gui.order" : "", + "consent.screen.text" : "" + } + }, { + "id" : "f7982b52-1c48-4223-890a-f3481b7555d3", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "consent.screen.text" : "", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "51767126-96ab-49c9-bb8c-d2410a77f9fc", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "b5774fa9-0652-404b-a4ac-091e9dc9d1c7", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "2aa1dc55-7b70-47b1-84e9-a1cd75d149be", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + }, { + "id" : "b37af7b6-1f23-43b5-b11f-cc6372ea04da", + "name" : "openid", + "description" : "", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false", + "gui.order" : "", + "consent.screen.text" : "" + } + }, { + "id" : "fb7f4864-7581-4935-a454-d389972b3d8b", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "consent.screen.text" : "${rolesScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "81d1637b-f15e-4a1e-9f9b-048633ce1293", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "3171a93e-2d1f-4b62-850d-34729883d2f5", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "f4eb8f3e-1168-4eb8-98d5-ae83d5b31fab", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr", "basic", "openid" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt", "edfi_admin_api/tenant_access", "edfi_admin_api/full_access", "edfiadmin/full-access", "edfi_admin_api/worker" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "referrerPolicy" : "no-referrer", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "5fa87355-e5ac-4a94-a0b4-436d360e34fa", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "a2a0c9be-d28f-420e-b2aa-22803c094992", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "oidc-address-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper" ] + } + }, { + "id" : "930f2347-f515-4bf8-9b03-1f3672b0018f", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper" ] + } + }, { + "id" : "a7119215-57a8-46dc-a20e-9b0f31d4dfd5", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "29d0f4a2-6251-4b29-bcc8-ba55e0a15429", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "e5239dbc-6b50-434e-99cf-5b44f1859ce3", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "a64d28c3-20d9-4cef-9089-d11e2a48572f", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "fc682d3a-9936-471d-8feb-6c998ab1f1d1", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + } ], + "org.keycloak.userprofile.UserProfileProvider" : [ { + "id" : "c451603c-8370-48db-a60b-2514850467e9", + "providerId" : "declarative-user-profile", + "subComponents" : { }, + "config" : { + "kc.user.profile.config" : [ "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}],\"unmanagedAttributePolicy\":\"ENABLED\"}" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "27398153-5b63-4e94-911e-68ff2ff1b1f6", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "67ec4ad5-5d86-4033-b9a1-ad9a1f53706c" ], + "secret" : [ "VKzPeq9CgPbt4xiUV3L3VA" ], + "priority" : [ "100" ] + } + }, { + "id" : "9d13b713-1f4e-4f10-8e5d-a485cbeb2f94", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpAIBAAKCAQEAzmga2JS3mIhyuhatxkNjk8BP+GY9f0SfevhdsBlTJnfCeCHmhnalYXtNBXq74cPtItLfxmVx3TE4p69xggUegDUhgI6dBInmEUw4qot42bihSghrB9r0AkR2DVyhPBoaWaMp4MFHwGXS7YFWpIwNdf2H2+8QCQXfW2bqQ3pPbiObCVFJn+jZh8p0jcZuK+1oOApDbsqGXs8rcC+qIkEs4Cp52SgsEuJbFYVLb93HT+f8JyGbFCIDSRt7YakgRfe24jXF94eEv0ln3MfWnpu+Expe1U7Pvpcm7pOM6wrvX8MxIiObki2wkp+TXcnydi22Au1Wl1u/Pj116wDDh4aMCQIDAQABAoIBABl/gtF+ja+qK4IRiyYS7Rozh62H89DlCU6HoAtBFaSK5tswVOoHfiastLcYkl0xdvn2Uu6PWaGZkwYkBkGpoDr2VU0sUPlej61TXSy0ThmXVVVv9Mjjo4EkM/EyoDhsgRTRszqwpcx+x6/PaoSpSIBq8X2Z6sEymIUSEZ6EXYudYZ0zNKMTRKw8Q62MkZnR/vWK40QOLpe6/esoZ6Nf8NrjWIa8bpgxVQblzKG95uz0MrJNSMcP0lX/eyIRQly2ndpQ5ECYoCq9Jh8GBQpO1a7WHgy4wyuC5OGuj+Mrqwq4C+XN+nOvHHXotp+ohDfHwCFj9AqgNyrPjeYIHHNchoECgYEA6NIG81o0vmcbEPmM7dNjOAz9VULtUzAs7jopMUUWz1FilsgwMB8HWJe52eS0mHyiyaHcvXHzPNnXpljMtbdB7NXbNXv9F12Vz6XgPM8x1TJX/qMm9DOHc8iMxjHzkhC0bN63nN3yYlBxPrAzDbhmAd0FgnYkge7g5z71bKlY8KsCgYEA4vTcfgrEDPo0c3Va62aXkhR6cDp9fuJw/EH3tjb7Lin31533T0jal3rsN6EWsgYEcxLuAGe6nsnEzuodCPowZVbD18WBS+JK1xIYCp86WkeYe20iS7lqP6bt76YBqoOTneF3HgTgvWd0w7e8Na45SZ0s+Bh6+i3B+T8UXhgTfhsCgYEAvJwJDXPybp1Qi/vec3xgULQVNtE1OcM5YvSzEjsKM4GXANfEUfeRF/Aym5Zvx2iQ6A+8p6x8hLtlvFkL3dpMCfVl1HpRmnAyQTn74sdnc3OJXbwGfqaQAPhjMtycmaP2xovW67nR9lt8onBTq7XKzY+kOwBCqxA+GpgicpjueGkCgYEAsHq5LeWPN4wj1xgNX/kBqZz07Gn/Bfhs5SsixPUR8n6981MpaOR83ce/AS7pJ/tX4Gd63KPMUQcSW9deRonNsi960EELXoeTrJOsUC4qwOLlZiyhu4JtZRuX738F0qLH2b/l0Vj3rTBScpJf7toQP/bRbgYTMpD8QaVUYCqWlbsCgYBxs8XlPwH/gBMC2iIG7FsCD0gLInjdSY9ZqAwvKbLWCr9TD1vg/9OcthDCL7jYqlTNQbpYoFsrw3FXQTfswljEI9HKdX5SBf4GP5DlDU6npsalXQv+s/KUBPUCkLXbxcbqVZuPQkzzzjH7cKO0LREGkbDmnH/jx5dALYv9sq9dLw==" ], + "keyUse" : [ "SIG" ], + "certificate" : [ "MIICnTCCAYUCBgGSi09rqDANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTAxNDEzNTMxMFoXDTM0MTAxNDEzNTQ1MFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM5oGtiUt5iIcroWrcZDY5PAT/hmPX9En3r4XbAZUyZ3wngh5oZ2pWF7TQV6u+HD7SLS38Zlcd0xOKevcYIFHoA1IYCOnQSJ5hFMOKqLeNm4oUoIawfa9AJEdg1coTwaGlmjKeDBR8Bl0u2BVqSMDXX9h9vvEAkF31tm6kN6T24jmwlRSZ/o2YfKdI3GbivtaDgKQ27Khl7PK3AvqiJBLOAqedkoLBLiWxWFS2/dx0/n/CchmxQiA0kbe2GpIEX3tuI1xfeHhL9JZ9zH1p6bvhMaXtVOz76XJu6TjOsK71/DMSIjm5ItsJKfk13J8nYttgLtVpdbvz49desAw4eGjAkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAHd62HzHI3eCrFOC/LD+HVyNF2Y9a29nSXSJaLZPwSXNOYsMD8osGqhG2RzOpQJUjxU9VG4eOgOQRAqQNIuSB9lOILE2vfvPbiHhjOsaCdcYOBu3Ow66qUm59aAXxLCh4zWoRSOg7fJdLLk6txQIorru+KkB9oKjuPNbwhU7WqwjBK72TJVqm9UuFUQCHfiVXqw4HdH2mbwWK9a8erG1M/CLLjqRa0opS0vQCCoDqD+pudCZDe4t9J7KLIi+IiJkJy5KBW4Z5BVwdxyWQ+AFp5Ud0igwdwOogz3VxHE5jPvHE/PWsYE+M/54X/w+BNIdKGmfeqhw6CS0DwpdNffGokQ==" ], + "priority" : [ "100" ] + } + }, { + "id" : "c86b780c-8a4b-4997-9322-f78d30f135d3", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "d54bf242-06ed-4cf8-95b4-38cebfa7f70e" ], + "secret" : [ "RyLed5-HgmVvlOrYYn3ThPI95PKSxN15iWTvqpylNusQTQhz5N-cTEpzFuk7KZCo1--Q6oFsNM5MbwkE0SPCMzZfgGwJ1oOzf8WOl9l8UPhxjgbMTkyR-ebgUCiG9TYPCA9nLi8vp4K-M0yxk-NGqlK3EzkZrVXJUITvoPk0S4Y" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + }, { + "id" : "9e3c42da-e459-477c-acef-ee02693126fc", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAsaJP5TeFYADnZxpa5SPabDm8KqYmQaxFYLa7Ecps3FRBcTz8isJ/OdYTb2un1fvMttG5b9tgUn/kglhG+06+2SOaxntuLXjnaTlySVjCM176lnkJBCLHTSBcrzVjEUD1vPaW5rYowfGhG7KkaYJudGzJr5+PncqURYV6VwOEX/1L8Ll63tlKkZeS/+FPrKMNuUSB9HE4TyKNku8GPh7g0p2tR2ENuwi1qjKJl0/YK+hA2o6q4ON9h/kUBmgR2PL/RLa1WliBFDgYP2ecXqBgYUWfm5VZX0H66y5JPw3wsn3nrfMy8Hhyu8DmW7XdZ0pSA/dYMXMQpLo+8TlU3O95JwIDAQABAoIBABmplVlMGByyhLvEdvU7pMaV4LVYo5ZBSRSb0ZLUZ728/mdZpDJ3vuGgJ9ak+u5THlQbIGskilng2CZsknSj/7ZnCmk6U8SF/wiYoAYwqH27bwBmJANQ8vIVhs5ZLCLF+7p89XgHQQQj4F6cBFyp10NCpDCXIKxvI9SXYAFKJMmpFJ7TWaKUhkR1bNwdMSCRFnJj6T+DpGDfH4bE77JCGZ314EEwF9UF407NB2rc6njtgtQJF2sH+LxkxWdrGYYCfxOAhg4f3ERYEevL+bubVrzWzmYl6yrUrru9jrUHpu51vqjWCPbCC9o8EOZd7GmCW6wL6C9AAozgUuiEFxQmj+ECgYEA4w+Ewb0/b2ddHO3LtDaIY+W338ZCJK3SRgf+nWMJXZOTcwWLUM90icXOFINGnSaxjTVE/7U4GsdYX88PS/CpjJLcOy9ZIwBVI/N+Ic+0jQ/6GFE4WkP3nYjTTd4HQ0/KyGrzZ0cg6Sg9Hi562CZd7n/zUH/gjx7uIkiZ1y+okfUCgYEAyEYdPcij5Dkpcgb/S0WXZDB/ctMYJqa8GRf1iMhu2eAz6j+plWn2zHtdsRtF+hQH/GQubbxlTvNPpe9Nf+bhX+4bHG9oNkWFH6t3++vni0hGylHs6eNhCOeXzmzb9SR+NGPb8q7+Ib2ya2xQ5o2jPq2qlKQMPQmPHnl+uzW5ASsCgYATPvZ+wX7F/5np4NIaZWDsLsWGJzTmDQG8w+K2DnX1w5OE9VxYrizvnJ+aJ4JtwaDdGj3N+ttVkb4Q6OkgA7crXitirOG+UMHmuTXgwc1JA6isNP400HwFk1ptyDsPXo0OrdoRz/qfKLT9gO0SszXVCG1kTmKHXxTKPSMlFToRnQKBgFu2oBV3GHafJretNKQHznh3LSYsPVcV6/Wh3zMwdnMYFS4k0Ug3Kmoh5JoCX11SZwXR6uEd8VbiXYsjt13nITaJF6lB63tPAtOp9lxLf9Fs294y/kdtdTZauE4z09bDS3g6crMEKCTTdi1zgOjT2/oMFZ3UMjaRdhUClmKtC3fZAoGBANZ8vUtAukZC+bwDQ+vSvyO6kJxlV3QHxBYVleNgNzPuk/HSQOM576wDm8cok+VMTdeU8Gmrks1muRogB9GHkVyWfzr6DRbO3+RTrptprEiQ0LsQILusqVfZoFGebmv9PGV80N5hgt1qhA8j4vzYFrna2LHWedlCdxhuKX2hg1a/" ], + "keyUse" : [ "ENC" ], + "certificate" : [ "MIICnTCCAYUCBgGSi09s7jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTAxNDEzNTMxMVoXDTM0MTAxNDEzNTQ1MVowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGiT+U3hWAA52caWuUj2mw5vCqmJkGsRWC2uxHKbNxUQXE8/IrCfznWE29rp9X7zLbRuW/bYFJ/5IJYRvtOvtkjmsZ7bi1452k5cklYwjNe+pZ5CQQix00gXK81YxFA9bz2lua2KMHxoRuypGmCbnRsya+fj53KlEWFelcDhF/9S/C5et7ZSpGXkv/hT6yjDblEgfRxOE8ijZLvBj4e4NKdrUdhDbsItaoyiZdP2CvoQNqOquDjfYf5FAZoEdjy/0S2tVpYgRQ4GD9nnF6gYGFFn5uVWV9B+usuST8N8LJ9563zMvB4crvA5lu13WdKUgP3WDFzEKS6PvE5VNzveScCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAk1pKVeymYKOv1S4xXrf2n0REnL85/315dX1ZjFbTkSyAPKmiSN5zU5sIuaU1zFW86uIcf2IoaqpMcUdstDbGvzo1Fq0qenkMnM7Z5o1yBxLIa9b5mYTflo6nEKBhNCDRyHLsDTxgPWLhIh3njJ64KIihaON2w/6GIJ8aHlqB+tJoX9toHadRE8RcrlDrAuA8LtVwEFpgITiDYiGPDhfZCxZxiddf1JdUcnMXuNWe95VcjWf0crUCiEcgUqNmdcb1vIMjtE96ugymMPb1J3RmQ8Cueby7kk0GfCvJSzDkhhfsxqgMXBqzoE75YO6vmLp3L44ccQ/F5ZMis9q60lD7Dw==" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + }, { + "id" : "4dda4abd-ba7f-44f0-a03e-a06bc221d31c", + "name" : "hmac-generated-hs512", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "c4c79ac7-036d-46b5-93ed-1086d9b06cf6" ], + "secret" : [ "bGeCSAMdyKCBExUpkdHg1ix9Sr9avhU9syVsmkCodfg4uVclEFod3sTmdpsN66Ox1iCVbb5BSt3q1PqMNDlEz11tMg2sGK8BFWTyXdiUWiEe1T49zNKyGBd-Hvo0iN-69Fi7F8GrXGZwoaY7qDcjM-ZE9AJNqVEjZZjWZ-2qe4k" ], + "priority" : [ "100" ], + "algorithm" : [ "HS512" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "f8b42759-a084-476a-b480-9750242a9f1d", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "2f1da14b-68dc-4202-beb6-7756cec39adf", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "6674803b-06d1-42f6-bbe5-17e3afab523b", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "eefc4ec5-e87b-466d-9d5f-fd0481fd4080", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "15502877-a537-4367-a5c1-2c9ff6624d24", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "6dc59805-b06f-4ee3-bedc-25fdec8bef60", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "abc0e6d4-4fbb-43c0-9801-44e269258718", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "489da63f-8ab9-4210-bcd7-1a295323e952", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "79c94038-6778-46ac-ab05-8483c32cc86f", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "0bf58859-5fc8-4994-af90-d2328a9e1980", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "29cca532-f5cb-4e8f-89fc-b4ee40f606ea", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "0327ad7e-65a9-45b3-9340-361c2c1b876f", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "2120393e-508e-43aa-9f26-f70856296236", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "f48ee6d2-3aa8-43e0-9335-e6384f3844a0", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "683d466c-08ca-4500-98d7-6974df5f585b", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "a024668b-e2c7-4d80-95b8-adaa16965ddb", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "38b9c486-d2a5-4fef-af03-ad98d96cb905", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "eaf5726d-ce9e-44f7-ba32-c495bf858932", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "941074e4-9a29-48bb-9c24-54cba2dd330a", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "15e16220-866b-4f25-ac6f-f458d95ced8b", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "delete_credential", + "name" : "Delete Credential", + "providerId" : "delete_credential", + "enabled" : true, + "defaultAction" : false, + "priority" : 100, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "firstBrokerLoginFlow" : "first broker login", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "clientOfflineSessionIdleTimeout" : "0", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false" + }, + "keycloakVersion" : "26.0.8", + "userManagedAccessAllowed" : false, + "organizationsEnabled" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} ] diff --git a/Docker/README.md b/Docker/README.md index 1863a3852..de5e08c4a 100644 --- a/Docker/README.md +++ b/Docker/README.md @@ -2,73 +2,46 @@ ## Development environment -### PostgreSQL Containers +1. `dev.pgsql.Dockerfile` -* dev.pgsql.Dockerfile + The purpose of this file is to facilitate the setup of Admin API docker image in + the development environment, allowing for local testing with latest changes using Postgresql. + It utilizes the assets and dlls from "Docker\Application\EdFi.Ods.AdminApi" + folder. - The purpose of this file is to facilitate the setup of Admin API docker - image in the development environment, allowing for local testing with latest - changes. It utilizes the assets and dlls from - "Docker\Application\EdFi.Ods.AdminApi" folder. +2. `dev.pgsql.Dockerfile` -* db.pgsql.admin.Dockerfile + The purpose of this file is to facilitate the setup of Admin API docker image in + the development environment, allowing for local testing with latest changes using MSSQL. + It utilizes the assets and dlls from "Docker\Application\EdFi.Ods.AdminApi" + folder. - Purpose of this file to setup the EdFi_Admin database image which includes - Admin API specific tables. It utilizes database artifacts located at - "Docker\Application\EdFi.Ods.AdminApi\Artifacts\PgSql\Structure\Admin". - -### MSSQL Containers - -* dev.mssql.Dockerfile - - The purpose of this file is to facilitate the setup of Admin API docker - image in the development environment, allowing for local testing with latest - changes. It utilizes the assets and dlls from - "Docker\Application\EdFi.Ods.AdminApi" folder. +3. `db.pgsql.admin.Dockerfile` -* db.mssql.admin.Dockerfile - - Purpose of this file to setup the EdFi_Admin database image which includes - Admin API specific tables. It utilizes database artifacts located at - "Docker\Application\EdFi.Ods.AdminApi\Artifacts\MsSql\Structure\Admin". - -> [!NOTE] -> The "EdFi.Ods.AdminApi" application folder and "Nuget.config" file will be -> copied over, either manually or through the execution of a script(`build.ps1 -> -Command "CopyApplicationFilesToDockerContext"`), to the "Application" folder -> within the "Docker" directory. + Purpose of this file to setup the EdFi_Admin database image which includes Admin + API specific tables. It utilizes database artifacts located at + "Docker\Application\EdFi.Ods.AdminApi\Artifacts\PgSql\Structure\Admin". ## Non-development environments -### PostgreSQL container +1. `api.pgsql.Dockerfile` -* api.pgsql.Dockerfile - - File for setting up Admin API docker image with assets and dlls sourced from + File for setting up Admin API and Postgres docker image with assets and dlls sourced from "EdFi.Suite3.ODS.AdminApi" nuget package(from - https://pkgs.dev.azure.com/ed-fi-alliance). - -* \Docker\Settings\DB-Admin\pgsql\Dockerfile - - This file to setup the EdFi_Admin database image which includes Admin API - specific tables. The database artifacts will be downloaded from - "EdFi.Suite3.ODS.AdminApi" nuget package(from - https://pkgs.dev.azure.com/ed-fi-alliance). - -### MSSQL container + ) -* api.mssql.Dockerfile +2. `api.mssql.Dockerfile` - File for setting up Admin API docker image with assets and dlls sourced from + File for setting up Admin API and MSSQL docker image with assets and dlls sourced from "EdFi.Suite3.ODS.AdminApi" nuget package(from - https://pkgs.dev.azure.com/ed-fi-alliance). + ) -* \Docker\Settings\DB-Admin\mssql\Dockerfile +3. `\Docker\Settings\DB-Admin\pgsql\Dockerfile` This file to setup the EdFi_Admin database image which includes Admin API specific tables. The database artifacts will be downloaded from "EdFi.Suite3.ODS.AdminApi" nuget package(from - https://pkgs.dev.azure.com/ed-fi-alliance). + ). For detailed instructions on setting up docker containers, please refer [docker.md](../docs/docker.md). diff --git a/Docker/Settings/DB-Admin/mssql/Dockerfile b/Docker/Settings/V1/DB-Admin/mssql/Dockerfile similarity index 99% rename from Docker/Settings/DB-Admin/mssql/Dockerfile rename to Docker/Settings/V1/DB-Admin/mssql/Dockerfile index 11eb4352e..b69345ce0 100644 --- a/Docker/Settings/DB-Admin/mssql/Dockerfile +++ b/Docker/Settings/V1/DB-Admin/mssql/Dockerfile @@ -10,7 +10,7 @@ LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " ENV MSSQL_DB=master -ARG ADMIN_API_VERSION=1.4.3 +ARG ADMIN_API_VERSION=1.4.2 ARG STANDARD_VERSION="4.0.0" ARG ADMIN_VERSION="6.2.536" ARG SECURITY_VERSION="6.2.568" diff --git a/Docker/Settings/DB-Admin/mssql/entrypoint.sh b/Docker/Settings/V1/DB-Admin/mssql/entrypoint.sh similarity index 100% rename from Docker/Settings/DB-Admin/mssql/entrypoint.sh rename to Docker/Settings/V1/DB-Admin/mssql/entrypoint.sh diff --git a/Docker/Settings/DB-Admin/mssql/healthcheck.sh b/Docker/Settings/V1/DB-Admin/mssql/healthcheck.sh similarity index 100% rename from Docker/Settings/DB-Admin/mssql/healthcheck.sh rename to Docker/Settings/V1/DB-Admin/mssql/healthcheck.sh diff --git a/Docker/Settings/DB-Admin/mssql/init-database.sh b/Docker/Settings/V1/DB-Admin/mssql/init-database.sh similarity index 100% rename from Docker/Settings/DB-Admin/mssql/init-database.sh rename to Docker/Settings/V1/DB-Admin/mssql/init-database.sh diff --git a/Docker/Settings/DB-Admin/pgsql/Dockerfile b/Docker/Settings/V1/DB-Admin/pgsql/Dockerfile similarity index 72% rename from Docker/Settings/DB-Admin/pgsql/Dockerfile rename to Docker/Settings/V1/DB-Admin/pgsql/Dockerfile index f4e9f3ebf..e52fb7c06 100644 --- a/Docker/Settings/DB-Admin/pgsql/Dockerfile +++ b/Docker/Settings/V1/DB-Admin/pgsql/Dockerfile @@ -7,15 +7,18 @@ FROM edfialliance/ods-api-db-admin:v2.3.5@sha256:c9a3b50f16f60e6a126d3bd37b2cb1d52e1fb0014f88d67193fb03e4414b9d98 AS base LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " +ARG POSTGRES_USER=postgres ENV POSTGRES_USER=${POSTGRES_USER} -ENV POSTGRES_PASSWORD=${POSTGRES_PASSWORD} ENV POSTGRES_DB=postgres -ARG VERSION="1.4.3" +ARG VERSION +ARG ADMIN_API_VERSION +ENV VERSION="${ADMIN_API_VERSION:-2.2.0}" USER root -RUN apk upgrade --no-cache && apk add dos2unix=~7 unzip=~6 openssl -USER postgres +COPY run-adminapi-migrations.sh /docker-entrypoint-initdb.d/3-run-adminapi-migrations.sh +RUN apk add --no-cache dos2unix=~7 unzip=~6 openssl=~3 +USER ${POSTGRES_USER} FROM base AS setup @@ -25,10 +28,11 @@ RUN wget -nv -O /tmp/EdFi_AdminApi_Scripts.zip "https://pkgs.dev.azure.com/ed-fi cp -r /tmp/AdminApiScripts/AdminApi/Artifacts/PgSql/Structure/Admin/. /tmp/AdminApiScripts/PgSql/ && \ rm -f /tmp/EdFi_AdminApi_Scripts.zip && \ rm -r /tmp/AdminApiScripts/AdminApi &&\ + dos2unix /docker-entrypoint-initdb.d/3-run-adminapi-migrations.sh && \ dos2unix /tmp/AdminApiScripts/PgSql/* && \ - chmod -R 700 /tmp/AdminApiScripts/PgSql - -USER postgres + chmod -R 700 /tmp/AdminApiScripts/PgSql && \ + chmod 700 /docker-entrypoint-initdb.d/3-run-adminapi-migrations.sh +USER ${POSTGRES_USER} EXPOSE 5432 diff --git a/Docker/Settings/DB-Admin/pgsql/run-adminapi-migrations.sh b/Docker/Settings/V1/DB-Admin/pgsql/run-adminapi-migrations.sh similarity index 100% rename from Docker/Settings/DB-Admin/pgsql/run-adminapi-migrations.sh rename to Docker/Settings/V1/DB-Admin/pgsql/run-adminapi-migrations.sh diff --git a/Docker/Settings/DB-Ods/mssql/Dockerfile b/Docker/Settings/V1/DB-Ods/mssql/Dockerfile similarity index 100% rename from Docker/Settings/DB-Ods/mssql/Dockerfile rename to Docker/Settings/V1/DB-Ods/mssql/Dockerfile diff --git a/Docker/Settings/DB-Ods/mssql/init.sh b/Docker/Settings/V1/DB-Ods/mssql/init.sh similarity index 100% rename from Docker/Settings/DB-Ods/mssql/init.sh rename to Docker/Settings/V1/DB-Ods/mssql/init.sh diff --git a/Docker/Settings/DB-Ods/mssql/setup-db.sh b/Docker/Settings/V1/DB-Ods/mssql/setup-db.sh similarity index 100% rename from Docker/Settings/DB-Ods/mssql/setup-db.sh rename to Docker/Settings/V1/DB-Ods/mssql/setup-db.sh diff --git a/Docker/Settings/V1/gateway/Dockerfile b/Docker/Settings/V1/gateway/Dockerfile new file mode 100644 index 000000000..200c29e4a --- /dev/null +++ b/Docker/Settings/V1/gateway/Dockerfile @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +# Tag nginx:alpine3.20 +FROM nginx@sha256:2140dad235c130ac861018a4e13a6bc8aea3a35f3a40e20c1b060d51a7efd250 +LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " + +ARG ADMIN_API_VIRTUAL_NAME=adminapi +ARG ODS_VIRTUAL_NAME=api + +ENV ADMIN_API_VIRTUAL_NAME=${ADMIN_API_VIRTUAL_NAME} +ENV ODS_VIRTUAL_NAME=${ODS_VIRTUAL_NAME} + +COPY ./default.conf.template /etc/nginx/templates/ + +EXPOSE 443 diff --git a/Docker/Settings/gateway/adminapi-packaged.conf b/Docker/Settings/V1/gateway/adminapi-packaged.conf similarity index 100% rename from Docker/Settings/gateway/adminapi-packaged.conf rename to Docker/Settings/V1/gateway/adminapi-packaged.conf diff --git a/Docker/Settings/gateway/default.conf.template b/Docker/Settings/V1/gateway/default.conf.template similarity index 100% rename from Docker/Settings/gateway/default.conf.template rename to Docker/Settings/V1/gateway/default.conf.template diff --git a/Docker/Settings/V2/DB-Admin/mssql/Dockerfile b/Docker/Settings/V2/DB-Admin/mssql/Dockerfile new file mode 100644 index 000000000..37fbe87ad --- /dev/null +++ b/Docker/Settings/V2/DB-Admin/mssql/Dockerfile @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +FROM mcr.microsoft.com/mssql/server@sha256:d7f2c670f0cd807b4dc466b8887bd2b39a4561f624c154896f5564ea38efd13a AS base +USER root +RUN apt-get update && apt-get install unzip -y dos2unix busybox openssl libxml2 +LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " + +ENV MSSQL_USER=${SQLSERVER_USER} +ENV MSSQL_PASSWORD=${SQLSERVER_PASSWORD} +ENV MSSQL_DB=master + +ARG STANDARD_VERSION="5.0.0" +ARG ADMIN_VERSION="7.2.49" +ARG SECURITY_VERSION="7.2.48" +ARG ADMIN_API_VERSION +ENV ADMIN_API_VERSION="${ADMIN_API_VERSION:-2.2.2}" + +USER root +COPY healthcheck.sh /usr/local/bin/healthcheck.sh +RUN chmod +x /usr/local/bin/healthcheck.sh + +COPY init-database.sh /tmp/init/3-init-database.sh +COPY entrypoint.sh /tmp/init/entrypoint.sh + +RUN useradd -M -s /bin/bash -u 10099 -g 0 edfi && \ + mkdir -p -m 770 /var/opt/edfi && chgrp -R 0 /var/opt/edfi && \ + wget -nv -O /tmp/sqlpackage.zip "https://aka.ms/sqlpackage-linux" && \ + unzip -o /tmp/sqlpackage.zip -d /opt/sqlpackage && \ + chmod +x /opt/sqlpackage/sqlpackage +FROM base AS setup + +USER root +RUN wget -q -O /tmp/EdFi_Admin.zip "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_apis/packaging/feeds/EdFi/nuget/packages/EdFi.Database.Admin.BACPAC.Standard.${STANDARD_VERSION}/versions/${ADMIN_VERSION}/content" && \ + wget -q -O /tmp/EdFi_Security.zip "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_apis/packaging/feeds/EdFi/nuget/packages/EdFi.Database.Security.BACPAC.Standard.${STANDARD_VERSION}/versions/${SECURITY_VERSION}/content" && \ + wget -nv -O /tmp/EdFi_AdminApi_Scripts.zip "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_apis/packaging/feeds/EdFi/nuget/packages/EdFi.Suite3.ODS.AdminApi/versions/${ADMIN_API_VERSION}/content" && \ + unzip -p /tmp/EdFi_Admin.zip EdFi_Admin.bacpac > /tmp/EdFi_Admin.bacpac && \ + unzip -p /tmp/EdFi_Security.zip EdFi_Security.bacpac > /tmp/EdFi_Security.bacpac && \ + dos2unix /tmp/EdFi_Admin.bacpac && \ + dos2unix /tmp/EdFi_Security.bacpac && \ + dos2unix /tmp/init/3-init-database.sh && \ + chmod +x /tmp/init/3-init-database.sh && \ + # Admin + mkdir -p /tmp/AdminApiScripts/Admin/ && \ + unzip -o /tmp/EdFi_AdminApi_Scripts.zip AdminApi/Artifacts/MsSql/Structure/Admin/* -d /tmp/AdminApiScripts/Admin/ && \ + cp -r /tmp/AdminApiScripts/Admin/AdminApi/Artifacts/MsSql/Structure/Admin/. /tmp/AdminApiScripts/Admin/MsSql/ && \ + dos2unix /tmp/AdminApiScripts/Admin/MsSql/* && \ + chmod -R 777 /tmp/AdminApiScripts/Admin/MsSql && \ + # Security + mkdir -p /tmp/AdminApiScripts/Security/ && \ + unzip -o /tmp/EdFi_AdminApi_Scripts.zip AdminApi/Artifacts/MsSql/Structure/Security/* -d /tmp/AdminApiScripts/Security/ && \ + cp -r /tmp/AdminApiScripts/Security/AdminApi/Artifacts/MsSql/Structure/Security/. /tmp/AdminApiScripts/Security/MsSql/ && \ + dos2unix /tmp/AdminApiScripts/Security/MsSql/* && \ + chmod -R 777 /tmp/AdminApiScripts/Security/MsSql && \ + # Clean up + rm -f /tmp/EdFi_Admin.zip && \ + rm -f /tmp/EdFi_Security.zip && \ + rm -f /tmp/EdFi_AdminApi_Scripts.zip + +EXPOSE 1433 + +USER edfi + + +CMD ["/bin/bash", "/tmp/init/entrypoint.sh"] diff --git a/Docker/Settings/V2/DB-Admin/mssql/entrypoint.sh b/Docker/Settings/V2/DB-Admin/mssql/entrypoint.sh new file mode 100644 index 000000000..300b986f9 --- /dev/null +++ b/Docker/Settings/V2/DB-Admin/mssql/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. +# + +/tmp/init/3-init-database.sh & /opt/mssql/bin/sqlservr diff --git a/Docker/Settings/V2/DB-Admin/mssql/healthcheck.sh b/Docker/Settings/V2/DB-Admin/mssql/healthcheck.sh new file mode 100644 index 000000000..bf38fe66a --- /dev/null +++ b/Docker/Settings/V2/DB-Admin/mssql/healthcheck.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. +# +# 2>&1 +/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P $SA_PASSWORD -Q "SELECT 1" > /dev/null 2>&1 +if [ $? -eq 0 ]; then + echo "MSSQL is ready..." + exit 0 +else + echo "MSSQL is not ready..." + exit 1 +fi diff --git a/Docker/Settings/V2/DB-Admin/mssql/init-database.sh b/Docker/Settings/V2/DB-Admin/mssql/init-database.sh new file mode 100644 index 000000000..498304383 --- /dev/null +++ b/Docker/Settings/V2/DB-Admin/mssql/init-database.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. +# + +set -e +set +x +MSSQL_USER=$SQLSERVER_USER +MSSQL_PASSWORD=$SQLSERVER_PASSWORD +MSSQL_SA_PASSWORD=$SA_PASSWORD + +function does_edfi_admin_db_exist() { + until /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P $MSSQL_SA_PASSWORD -Q "SELECT 1" > /dev/null 2>&1 + do + >&2 echo "MSSQL is unavailable - sleeping" + sleep 10 + done + local result=$(/opt/mssql-tools18/bin/sqlcmd -S "(local)" -U "sa" -P $MSSQL_SA_PASSWORD -C -Q "IF EXISTS (SELECT name FROM sys.databases WHERE name = 'EdFi_Admin') PRINT 'Database exists' ELSE PRINT 'Database does not exist'" -h -1) + + if [[ "$result" == *"Database exists"* ]]; then + return 0 + else + return 1 + fi +} +echo "Database initialization..." +if ! does_edfi_admin_db_exist; then + echo "Creating base Admin and Security databases..." + /opt/sqlpackage/sqlpackage /Action:Import /tsn:"localhost" /tdn:"EdFi_Security" /tu:"sa" /tp:"$MSSQL_SA_PASSWORD" /sf:"/tmp/EdFi_Security.bacpac" /ttsc:true + /opt/sqlpackage/sqlpackage /Action:Import /tsn:"localhost" /tdn:"EdFi_Admin" /tu:"sa" /tp:"$MSSQL_SA_PASSWORD" /sf:"/tmp/EdFi_Admin.bacpac" /ttsc:true + # Force sorting by name following C language sort ordering, so that the sql scripts are run + # sequentially in the correct alphanumeric order + echo "Running Admin Api database migration scripts..." + + for FILE in `LANG=C ls /tmp/AdminApiScripts/Admin/MsSql/*.sql | sort -V` + do + echo "Running script: ${FILE}..." + /opt/mssql-tools18/bin/sqlcmd -S "localhost" -U "sa" -P "$MSSQL_SA_PASSWORD" -d "EdFi_Admin" -i $FILE -C + done + + for FILE in `LANG=C ls /tmp/AdminApiScripts/Security/MsSql/*.sql | sort -V` + do + echo "Running script: ${FILE}..." + /opt/mssql-tools18/bin/sqlcmd -S "localhost" -U "sa" -P "$MSSQL_SA_PASSWORD" -d "EdFi_Security" -i $FILE -C + done + + echo "Finish Admin Api database migration scripts..." + + echo "Creating database users..." + /opt/mssql-tools18/bin/sqlcmd -S "localhost" -C -U sa -P $MSSQL_SA_PASSWORD -Q "IF NOT EXISTS (SELECT * FROM master.sys.server_principals WHERE name = '$MSSQL_USER') BEGIN CREATE LOGIN [$MSSQL_USER] WITH PASSWORD = '$SQLSERVER_PASSWORD'; END; ALTER AUTHORIZATION ON DATABASE::EdFi_Security TO [$MSSQL_USER]; ALTER AUTHORIZATION ON DATABASE::EdFi_Admin TO [$MSSQL_USER];" +fi +echo "Database is initialized and ready to use." diff --git a/Docker/Settings/V2/DB-Admin/mssql/run-adminapi-migrations.sh b/Docker/Settings/V2/DB-Admin/mssql/run-adminapi-migrations.sh new file mode 100644 index 000000000..75a938e56 --- /dev/null +++ b/Docker/Settings/V2/DB-Admin/mssql/run-adminapi-migrations.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +set -e +set +x + +if [[ -z "$MSSQL_PORT" ]]; then + export MSSQL_PORT=1433 +fi + +# Force sorting by name following C language sort ordering, so that the sql scripts are run +# sequentially in the correct alphanumeric order +echo "Running Admin Api database migration scripts..." + +for FILE in `LANG=C ls /tmp/AdminApiScripts/Admin/MsSql/*.sql | sort -V` +do + /opt/mssql-tools18/bin/sqlcmd -S localhost,$MSSQL_PORT -U "$MSSQL_USER" -P "$MSSQL_PASSWORD" -d "EdFi_Admin" -i --file $FILE 1> /dev/null +done + +for FILE in `LANG=C ls /tmp/AdminApiScripts/Security/MsSql/*.sql | sort -V` +do + /opt/mssql-tools18/bin/sqlcmd -S localhost,$MSSQL_PORT -U "$MSSQL_USER" -P "$MSSQL_PASSWORD" -d "EdFi_Security" -i --file $FILE 1> /dev/null +done + diff --git a/Docker/Settings/V2/DB-Admin/pgsql/Dockerfile b/Docker/Settings/V2/DB-Admin/pgsql/Dockerfile new file mode 100644 index 000000000..bfcf97447 --- /dev/null +++ b/Docker/Settings/V2/DB-Admin/pgsql/Dockerfile @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +#edfialliance/ods-api-db-admin:7.3.1 +FROM edfialliance/ods-api-db-admin:7.3.1@sha256:9d6c6ad298f5eb2ea58d7b2c1c7ea5f6bdfcd12d90028b98fbfea4237a5610f2 AS base +LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " + +ARG POSTGRES_USER=postgres +ENV POSTGRES_USER=${POSTGRES_USER} +ENV POSTGRES_DB=postgres + +ARG VERSION +ARG ADMIN_API_VERSION +ENV VERSION="${ADMIN_API_VERSION:-2.2.0}" + +USER root +COPY run-adminapi-migrations.sh /docker-entrypoint-initdb.d/3-run-adminapi-migrations.sh +RUN apk add --no-cache dos2unix=7.5.2-r0 unzip=6.0-r15 && rm -rf /var/cache/apk/* +USER ${POSTGRES_USER} + +FROM base AS setup + +USER root +RUN wget -nv -O /tmp/EdFi_AdminApi_Scripts.zip "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_apis/packaging/feeds/EdFi/nuget/packages/EdFi.Suite3.ODS.AdminApi/versions/${VERSION}/content" && \ + # Admin + mkdir -p /tmp/AdminApiScripts/Admin/ && \ + unzip /tmp/EdFi_AdminApi_Scripts.zip AdminApi/Artifacts/PgSql/Structure/Admin/* -d /tmp/AdminApiScripts/Admin/ && \ + cp -r /tmp/AdminApiScripts/Admin/AdminApi/Artifacts/PgSql/Structure/Admin/. /tmp/AdminApiScripts/Admin/PgSql/ && \ + rm -r /tmp/AdminApiScripts/Admin/AdminApi && \ + dos2unix /docker-entrypoint-initdb.d/3-run-adminapi-migrations.sh && \ + dos2unix /tmp/AdminApiScripts/Admin/PgSql/* && \ + chmod -R 777 /tmp/AdminApiScripts/Admin/PgSql && \ + # Security + mkdir -p /tmp/AdminApiScripts/Security/ && \ + unzip /tmp/EdFi_AdminApi_Scripts.zip AdminApi/Artifacts/PgSql/Structure/Security/* -d /tmp/AdminApiScripts/Security/ && \ + cp -r /tmp/AdminApiScripts/Security/AdminApi/Artifacts/PgSql/Structure/Security/. /tmp/AdminApiScripts/Security/PgSql/ && \ + rm -r /tmp/AdminApiScripts/Security/AdminApi && \ + dos2unix /docker-entrypoint-initdb.d/3-run-adminapi-migrations.sh && \ + dos2unix /tmp/AdminApiScripts/Security/PgSql/* && \ + chmod -R 777 /tmp/AdminApiScripts/Security/PgSql && \ + # Clean up + rm -f /tmp/EdFi_AdminApi_Scripts.zip && \ + apk del dos2unix unzip + +USER ${POSTGRES_USER} + +EXPOSE 5432 + +CMD ["docker-entrypoint.sh", "postgres"] diff --git a/Docker/Settings/V2/DB-Admin/pgsql/run-adminapi-migrations.sh b/Docker/Settings/V2/DB-Admin/pgsql/run-adminapi-migrations.sh new file mode 100644 index 000000000..08be9b58e --- /dev/null +++ b/Docker/Settings/V2/DB-Admin/pgsql/run-adminapi-migrations.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +set -e +set +x + +if [[ -z "$POSTGRES_PORT" ]]; then + export POSTGRES_PORT=5432 +fi + +# Force sorting by name following C language sort ordering, so that the sql scripts are run +# sequentially in the correct alphanumeric order +echo "Running Admin Api database migration scripts..." + +for FILE in `LANG=C ls /tmp/AdminApiScripts/Admin/PgSql/*.sql | sort -V` +do + psql --no-password --username "$POSTGRES_USER" --port $POSTGRES_PORT --dbname "EdFi_Admin" --file $FILE 1> /dev/null +done + +for FILE in `LANG=C ls /tmp/AdminApiScripts/Security/PgSql/*.sql | sort -V` +do + psql --no-password --username "$POSTGRES_USER" --port $POSTGRES_PORT --dbname "EdFi_Security" --file $FILE 1> /dev/null +done diff --git a/Docker/Settings/V2/DB-Ods/mssql/Dockerfile b/Docker/Settings/V2/DB-Ods/mssql/Dockerfile new file mode 100644 index 000000000..05791ccc0 --- /dev/null +++ b/Docker/Settings/V2/DB-Ods/mssql/Dockerfile @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +# Base image with additional packages +FROM mcr.microsoft.com/mssql/server:2022-CU15-ubuntu-22.04@sha256:804e527187b16fe05c4321b58d76850966cbaf73e31942416ce0bbefe1b0eb63 AS base +USER root + +RUN ACCEPT_EULA=Y apt-get update -y && \ + ACCEPT_EULA=Y apt-get upgrade -y && \ + ACCEPT_EULA=Y apt-get -y install --no-install-recommends apt-utils -y unzip=6.0-26ubuntu3 dos2unix=7.4.2-2 && \ + rm -rf /var/lib/apt/lists/* && \ + echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections && \ + # These steps are needed in order to mount the database to a host volume + mkdir -p /var/opt/mssql/data /var/opt/mssql/log && \ + chown -R mssql: /var/opt/mssql/data /var/opt/mssql/log +USER mssql + +# Start a new layer so that the above layer can be cached +FROM base AS build +ARG ODS_VERSION +ARG TPDM_VERSION +ARG STANDARD_VERSION +ARG EXTENSION_VERSION +ARG ODS_URL=https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_apis/packaging/feeds/EdFi/nuget/packages/EdFi.Suite3.Ods.Minimal.Template.Standard.${STANDARD_VERSION}/versions/${ODS_VERSION}/content +ARG TPDM_URL=https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_apis/packaging/feeds/EdFi/nuget/packages/EdFi.Suite3.Ods.Minimal.Template.TPDM.Core.${EXTENSION_VERSION}.Standard.${STANDARD_VERSION}/versions/${TPDM_VERSION}/content + +LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " + +# These variables can be overwritten at runtime +ENV MSSQL_PID=Express + +USER root +WORKDIR /app + +RUN umask 0077 && \ + mkdir backups && \ + # Download and extract Minimal Template for core Ed-Fi Data Model + wget -q -O ./backups/OdsMinimalDatabase.zip ${ODS_URL} && \ + unzip -p ./backups/OdsMinimalDatabase.zip EdFi.Ods.Minimal.Template.bak > ./backups/EdFi_Ods_Minimal_Template.bak + + # Download and extract Minimal Template for Teacher Prep Data Model (TPDM) +RUN wget -q -O ./backups/TPDMOdsMinimalDatabase.zip ${TPDM_URL} && \ + unzip -p ./backups/TPDMOdsMinimalDatabase.zip EdFi.Ods.Minimal.Template.TPDM.Core.bak > ./backups/EdFi_Ods_Minimal_Template_TPDM_Core.bak && \ + rm -f ./backups/*.zip && \ + apt-get --purge autoremove unzip -y + +COPY --chmod=500 ./*.sh . + +RUN dos2unix ./*.sh && \ + chown -R mssql . && \ + apt-get --purge autoremove dos2unix -y + +EXPOSE 1433 +USER mssql +ENTRYPOINT ["/app/init.sh"] \ No newline at end of file diff --git a/Docker/Settings/V2/DB-Ods/mssql/init.sh b/Docker/Settings/V2/DB-Ods/mssql/init.sh new file mode 100644 index 000000000..0e25aa368 --- /dev/null +++ b/Docker/Settings/V2/DB-Ods/mssql/init.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +set -e +set +x + +# Export default values +export MSSQL_SA_PASSWORD=$SQLSERVER_PASSWORD +export ACCEPT_EULA=Y + +/app/setup-db.sh & + +/opt/mssql/bin/sqlservr \ No newline at end of file diff --git a/Docker/Settings/V2/DB-Ods/mssql/setup-db.sh b/Docker/Settings/V2/DB-Ods/mssql/setup-db.sh new file mode 100644 index 000000000..ed87a4b2a --- /dev/null +++ b/Docker/Settings/V2/DB-Ods/mssql/setup-db.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +set -e +set +x + +STATUS_SA=1 +STATUS_USER=1 +while [[ $STATUS_SA -ne 0 && $STATUS_USER -ne 0 ]]; do + >&2 echo "Waiting for server to be online... " + STATUS_SA=$(/opt/mssql-tools18/bin/sqlcmd -C -W -h -1 -U sa -P "${SQLSERVER_PASSWORD}" -Q "SET NOCOUNT ON; SELECT SUM(state) FROM sys.databases" > /dev/null 2>&1 || echo 1) + + STATUS_USER=$(/opt/mssql-tools18/bin/sqlcmd -C -W -h -1 -U ${SQLSERVER_USER} -P "${SQLSERVER_PASSWORD}" -Q "SET NOCOUNT ON; SELECT SUM(state) FROM sys.databases" > /dev/null 2>&1 || echo 1) + + sleep 10 +done + +echo "Configuring user..." +# If conneciton fails, it means we already have configured logins, so we can redirect the error to /dev/null +/opt/mssql-tools18/bin/sqlcmd -C -U sa -P "${SQLSERVER_PASSWORD}" -Q " + CREATE LOGIN ${SQLSERVER_USER} WITH PASSWORD = '${SQLSERVER_PASSWORD}'; + CREATE USER ${SQLSERVER_USER} FOR LOGIN ${SQLSERVER_USER}; + ALTER SERVER ROLE [sysadmin] ADD MEMBER ${SQLSERVER_USER}; + ALTER LOGIN [SA] DISABLE;" > /dev/null 2>&1 + +export MINIMAL_BACKUP=EdFi_Ods_Minimal_Template.bak + +if [[ "$TPDM_ENABLED" = true ]]; then + export MINIMAL_BACKUP=EdFi_Ods_Minimal_Template_TPDM_Core.bak +fi + +# If the EdFi_Ods_Minimal_Template is restored, we skip restoring it again +if [[ ! -f "/var/opt/mssql/data/EdFi_Ods_Minimal_Template.mdf" ]]; then + echo "Loading EdFi_Ods_Minimal_Template database from backup..." + /opt/mssql-tools18/bin/sqlcmd -C -U ${SQLSERVER_USER} -P ${SQLSERVER_PASSWORD} -Q " + RESTORE DATABASE [EdFi_Ods] FROM DISK = N'/app/backups/${MINIMAL_BACKUP}' + WITH MOVE 'EdFi_Ods_Populated_Template_Test' TO '/var/opt/mssql/data/EdFi_Ods_Minimal_Template.mdf', + MOVE 'EdFi_Ods_Populated_Template_Test_Log' TO '/var/opt/mssql/log/EdFi_Ods_Minimal_Template_log.ldf';" +fi \ No newline at end of file diff --git a/Docker/Settings/gateway/Dockerfile b/Docker/Settings/V2/gateway/Dockerfile similarity index 65% rename from Docker/Settings/gateway/Dockerfile rename to Docker/Settings/V2/gateway/Dockerfile index 4718eb3e3..e7bb8e43f 100644 --- a/Docker/Settings/gateway/Dockerfile +++ b/Docker/Settings/V2/gateway/Dockerfile @@ -3,13 +3,10 @@ # The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. # See the LICENSE and NOTICES files in the project root for more information. -# Tag nginx:alpine -FROM nginx@sha256:da9c94bec1da829ebd52431a84502ec471c8e548ffb2cedbf36260fd9bd1d4d3 +# Tag nginx:alpine3.20 +FROM nginx@sha256:2140dad235c130ac861018a4e13a6bc8aea3a35f3a40e20c1b060d51a7efd250 LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " -ENV ADMIN_API_VIRTUAL_NAME=${ADMIN_API_VIRTUAL_NAME:-adminapi} -ENV ODS_VIRTUAL_NAME=${ODS_VIRTUAL_NAME:-api} - COPY ./default.conf.template /etc/nginx/templates/ EXPOSE 443 diff --git a/Docker/Settings/V2/gateway/IDP.Dockerfile b/Docker/Settings/V2/gateway/IDP.Dockerfile new file mode 100644 index 000000000..a924a54f1 --- /dev/null +++ b/Docker/Settings/V2/gateway/IDP.Dockerfile @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Licensed to the Ed-Fi Alliance under one or more agreements + +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0 + +# See the LICENSE and NOTICES files in the project root for more information + +# Tag nginx:alpine3.20 + +FROM nginx@sha256:2140dad235c130ac861018a4e13a6bc8aea3a35f3a40e20c1b060d51a7efd250 +LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " + +COPY ./default_idp.conf.template /etc/nginx/templates/default.conf.template + +EXPOSE 443 diff --git a/Docker/Settings/V2/gateway/default.conf.template b/Docker/Settings/V2/gateway/default.conf.template new file mode 100644 index 000000000..9189bfa51 --- /dev/null +++ b/Docker/Settings/V2/gateway/default.conf.template @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +server { + listen 80 default_server; + + server_name _; + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name localhost; + + ssl_certificate /ssl/server.crt; + ssl_certificate_key /ssl/server.key; + + location /${ADMIN_API_VIRTUAL_NAME} { + client_max_body_size 20M; + proxy_pass http://${ADMIN_API_VIRTUAL_NAME}; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 443; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} diff --git a/Docker/Settings/V2/gateway/default_idp.conf.template b/Docker/Settings/V2/gateway/default_idp.conf.template new file mode 100644 index 000000000..69189bbd5 --- /dev/null +++ b/Docker/Settings/V2/gateway/default_idp.conf.template @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +server { + listen 80 default_server; + + server_name _; + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name localhost; + + ssl_certificate /ssl/server.crt; + ssl_certificate_key /ssl/server.key; + + location /${ADMIN_API_VIRTUAL_NAME} { + client_max_body_size 20M; + proxy_pass http://${ADMIN_API_VIRTUAL_NAME}; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 443; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location /auth { + proxy_pass http://ed-fi-idp-keycloak:8080/auth; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header Accept-Encoding ""; + # Increase proxy timeouts + proxy_connect_timeout 60s; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization'; + add_header Content-Security-Policy "frame-src 'self' https://locahost"; + } +} diff --git a/Docker/Settings/dev/adminapi-test-seeddata.sql b/Docker/Settings/dev/adminapi-test-seeddata.sql new file mode 100644 index 000000000..4d8001d56 --- /dev/null +++ b/Docker/Settings/dev/adminapi-test-seeddata.sql @@ -0,0 +1,3 @@ +-- LOAD DATAS +INSERT INTO dbo.odsinstances(name, instancetype, connectionstring) +VALUES ('Ods-test', 'OdsInstance', 'Host=localhost; Port=5432; Database=EdFi_Ods; username=username; Integrated Security=true; Application Name=AdminApi;'); diff --git a/Docker/Settings/dev/mssql/adminapi-test-seeddata.sql b/Docker/Settings/dev/mssql/adminapi-test-seeddata.sql new file mode 100644 index 000000000..fed63361f --- /dev/null +++ b/Docker/Settings/dev/mssql/adminapi-test-seeddata.sql @@ -0,0 +1,3 @@ +-- LOAD DATAS +INSERT INTO dbo.odsinstances(name, instancetype, connectionstring) +VALUES ('Ods-test', 'OdsInstance', 'Data Source=localhost;Initial Catalog=EdFi_Ods;Integrated Security=True;Encrypt=false;TrustServerCertificate=true; Application Name=AdminApi;'); diff --git a/Docker/Settings/dev/mssql/run.sh b/Docker/Settings/dev/mssql/run.sh index c204c430b..6724b72d4 100644 --- a/Docker/Settings/dev/mssql/run.sh +++ b/Docker/Settings/dev/mssql/run.sh @@ -7,10 +7,6 @@ set -e set +x -envsubst < /app/appsettings.Docker.mssql.json > /app/temp.json - -mv /app/temp.json /app/appsettings.json - if [[ -z "$ADMIN_WAIT_MSSQL_HOSTS" ]]; then # if there are no hosts to wait then fallback to $ADMIN_MSSQL_HOST export ADMIN_WAIT_MSSQL_HOSTS=$ADMIN_MSSQL_HOST @@ -34,4 +30,7 @@ if [[ -f /ssl/server.crt ]]; then update-ca-certificates fi +# Writing permissions for multitenant environment so the user can create tenants +chmod 664 /app/appsettings.json + dotnet EdFi.Ods.AdminApi.dll diff --git a/Docker/Settings/dev/pgsql/run.sh b/Docker/Settings/dev/pgsql/run.sh index 45de111be..4d06e4231 100644 --- a/Docker/Settings/dev/pgsql/run.sh +++ b/Docker/Settings/dev/pgsql/run.sh @@ -7,13 +7,56 @@ set -e set +x -envsubst < /app/appsettings.Docker.pgsql.json > /app/temp.json +if [[ -z "$ADMIN_WAIT_POSTGRES_HOSTS" ]]; then + # if there are no hosts to wait then fallback to $ODS_POSTGRES_HOST + export ADMIN_WAIT_POSTGRES_HOSTS=$ADMIN_POSTGRES_HOST +fi + +export ADMIN_WAIT_POSTGRES_HOSTS_ARR=($ADMIN_WAIT_POSTGRES_HOSTS) +for HOST in ${ADMIN_WAIT_POSTGRES_HOSTS_ARR[@]} +do + until PGPASSWORD=$POSTGRES_PASSWORD \ + PGHOST=$HOST \ + PGPORT=$POSTGRES_PORT \ + PGUSER=$POSTGRES_USER \ + pg_isready > /dev/null + do + >&2 echo "Admin '$HOST' is unavailable - sleeping" + sleep 10 + done +done -mv /app/temp.json /app/appsettings.json +>&2 echo "Postgres is up - executing command" +exec $cmd + +if [[ -z "$ADMIN_WAIT_POSTGRES_HOSTS" ]]; then + # if there are no hosts to wait then fallback to $ODS_POSTGRES_HOST + export ADMIN_WAIT_POSTGRES_HOSTS=$ADMIN_POSTGRES_HOST +fi + +export ADMIN_WAIT_POSTGRES_HOSTS_ARR=($ADMIN_WAIT_POSTGRES_HOSTS) +for HOST in ${ADMIN_WAIT_POSTGRES_HOSTS_ARR[@]} +do + until PGPASSWORD=$POSTGRES_PASSWORD \ + PGHOST=$HOST \ + PGPORT=$POSTGRES_PORT \ + PGUSER=$POSTGRES_USER \ + pg_isready > /dev/null + do + >&2 echo "Admin '$HOST' is unavailable - sleeping" + sleep 10 + done +done + +>&2 echo "Postgres is up - executing command" +exec $cmd if [[ -f /ssl/server.crt ]]; then cp /ssl/server.crt /usr/local/share/ca-certificates/ update-ca-certificates fi +# Writing permissions for multitenant environment so the user can create tenants +chmod 664 /app/appsettings.json + dotnet EdFi.Ods.AdminApi.dll diff --git a/Docker/Settings/mssql/appsettings.template.json b/Docker/Settings/mssql/appsettings.template.json deleted file mode 100644 index a3c9ac5a4..000000000 --- a/Docker/Settings/mssql/appsettings.template.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "AppSettings": { - "DatabaseEngine": "SqlServer", - "ApiStartupType": "$API_MODE", - "PathBase": "$ADMIN_API_VIRTUAL_NAME", - "OdsApiVersion": "$ODS_API_VERSION", - "DefaultPageSizeOffset": 0, - "DefaultPageSizeLimit": 25 - }, - "Authentication": { - "Authority": "$AUTHORITY", - "IssuerUrl": "$ISSUER_URL", - "SigningKey": "$SIGNING_KEY", - "AllowRegistration": true - }, - "EnableSwagger": true, - "EnableDockerEnvironment": true, - "ConnectionStrings": { - "Admin": "server=$SQLSERVER_ADMIN_DATASOURCE;database=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD;Integrated Security=false;Application Name=Ed-Fi ODS/API AdminApi;Encrypt=false", - "Security": "server=$SQLSERVER_ADMIN_DATASOURCE;database=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD;Integrated Security=false;Application Name=Ed-Fi ODS/API AdminApi;Encrypt=false" - }, - "Log4NetCore": { - "Log4NetConfigFileName": "./log4net.config" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} diff --git a/Docker/Settings/mssql/env.example b/Docker/Settings/mssql/env.example index cee34d5cb..f98a1b1dc 100644 --- a/Docker/Settings/mssql/env.example +++ b/Docker/Settings/mssql/env.example @@ -1,11 +1,13 @@ -API_MODE= ADMIN_API_VIRTUAL_NAME= -ODS_API_VERSION= # For Authentication AUTHORITY= ISSUER_URL= -SIGNING_KEY= +SIGNING_KEY= + +PAGING_OFFSET=0 +PAGING_LIMIT=25 +PREVENT_DUPLICATE_APPLICATIONS=false # For SQL Server only SQLSERVER_ODS_DATASOURCE= \ No newline at end of file diff --git a/Docker/Settings/mssql/run.sh b/Docker/Settings/mssql/run.sh index af023c81f..aab016500 100644 --- a/Docker/Settings/mssql/run.sh +++ b/Docker/Settings/mssql/run.sh @@ -7,9 +7,6 @@ set -e set +x -envsubst < /app/appsettings.template.json > /app/temp.json -mv /app/temp.json /app/appsettings.json - if [[ -z "$ADMIN_WAIT_MSSQL_HOSTS" ]]; then # if there are no hosts to wait then fallback to $ADMIN_MSSQL_HOST export ADMIN_WAIT_MSSQL_HOSTS=$ADMIN_MSSQL_HOST @@ -28,9 +25,13 @@ done >&2 echo "MSSQL is up - executing command" exec $cmd + if [[ -f /ssl/server.crt ]]; then cp /ssl/server.crt /usr/local/share/ca-certificates/ update-ca-certificates fi +# Writing permissions for multitenant environment so the user can create tenants +chmod 664 /app/appsettings.json + dotnet EdFi.Ods.AdminApi.dll diff --git a/Docker/Settings/pgsql/appsettings.template.json b/Docker/Settings/pgsql/appsettings.template.json deleted file mode 100644 index f6c0b5741..000000000 --- a/Docker/Settings/pgsql/appsettings.template.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "AppSettings": { - "DatabaseEngine": "PostgreSQL", - "ApiStartupType": "$API_MODE", - "PathBase": "$ADMIN_API_VIRTUAL_NAME", - "OdsApiVersion": "$ODS_API_VERSION", - "DefaultPageSizeOffset": 0, - "DefaultPageSizeLimit": 25 - }, - "Authentication": { - "Authority": "$AUTHORITY", - "IssuerUrl": "$ISSUER_URL", - "SigningKey": "$SIGNING_KEY", - "AllowRegistration": true - }, - "EnableSwagger": true, - "EnableDockerEnvironment": true, - "ConnectionStrings": { - "Admin": "host=${ADMIN_POSTGRES_HOST};port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=false", - "Security": "host=${ADMIN_POSTGRES_HOST};port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=false" - }, - "Log4NetCore": { - "Log4NetConfigFileName": "./log4net.config" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} diff --git a/Docker/Settings/pgsql/env.example b/Docker/Settings/pgsql/env.example index 71b8f6b7d..74affa008 100644 --- a/Docker/Settings/pgsql/env.example +++ b/Docker/Settings/pgsql/env.example @@ -1,16 +1,34 @@ -API_MODE= ADMIN_API_VIRTUAL_NAME= -ODS_API_VERSION= # For Authentication AUTHORITY= ISSUER_URL= -SIGNING_KEY= +SIGNING_KEY= + +PAGING_OFFSET=0 +PAGING_LIMIT=25 +PREVENT_DUPLICATE_APPLICATIONS=false # For Postgres only POSTGRES_USER= POSTGRES_PASSWORD= POSTGRES_PORT= + ADMIN_POSTGRES_HOST= \ No newline at end of file diff --git a/Docker/Settings/pgsql/run.sh b/Docker/Settings/pgsql/run.sh old mode 100644 new mode 100755 index 51ecb993f..00c92195e --- a/Docker/Settings/pgsql/run.sh +++ b/Docker/Settings/pgsql/run.sh @@ -7,14 +7,23 @@ set -e set +x -envsubst < /app/appsettings.template.json > /app/temp.json - -mv /app/temp.json /app/appsettings.json +if [[ -z "$ADMIN_WAIT_POSTGRES_HOSTS" ]]; then + # if there are no hosts to wait then fallback to $ODS_POSTGRES_HOST + export ADMIN_WAIT_POSTGRES_HOSTS=$ADMIN_POSTGRES_HOST +fi -until PGPASSWORD=$POSTGRES_PASSWORD psql -h $ADMIN_POSTGRES_HOST -p $POSTGRES_PORT -U $POSTGRES_USER -c '\q'; +export ADMIN_WAIT_POSTGRES_HOSTS_ARR=($ADMIN_WAIT_POSTGRES_HOSTS) +for HOST in ${ADMIN_WAIT_POSTGRES_HOSTS_ARR[@]} do - >&2 echo "Admin Postgres is unavailable - sleeping" - sleep 10 + until PGPASSWORD=$POSTGRES_PASSWORD \ + PGHOST=$HOST \ + PGPORT=$POSTGRES_PORT \ + PGUSER=$POSTGRES_USER \ + pg_isready > /dev/null + do + >&2 echo "Admin '$HOST' is unavailable - sleeping" + sleep 10 + done done >&2 echo "Postgres is up - executing command" @@ -25,4 +34,7 @@ if [[ -f /ssl/server.crt ]]; then update-ca-certificates fi +# Writing permissions for multitenant environment so the user can create tenants +chmod 664 /app/appsettings.json + dotnet EdFi.Ods.AdminApi.dll diff --git a/Docker/Settings/ssl/generate-certificate.sh b/Docker/Settings/ssl/generate-certificate.sh index 16abf0322..c86814eea 100644 --- a/Docker/Settings/ssl/generate-certificate.sh +++ b/Docker/Settings/ssl/generate-certificate.sh @@ -6,7 +6,7 @@ # See the LICENSE and NOTICES files in the project root for more information. set -e -set -x +set +x -openssl dhparam -out dhparam.pem 2048 -openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365 -addext "subjectAltName = DNS:localhost,DNS:nginx" +openssl dhparam -out dhparam.pem 4096 +openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt -days 365 -addext "subjectAltName = DNS:localhost,DNS:nginx" diff --git a/Docker/V1/Compose/mssql/compose-build-binaries.yml b/Docker/V1/Compose/mssql/compose-build-binaries.yml new file mode 100644 index 000000000..7e1514417 --- /dev/null +++ b/Docker/V1/Compose/mssql/compose-build-binaries.yml @@ -0,0 +1,107 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +version: "3.8" + +services: + nginx: + build: + context: ../../../Settings/V1/gateway/ + dockerfile: Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ports: + - "443:443" + container_name: ed-fi-gateway-adminapi + restart: always + hostname: nginx + volumes: + - ../../../Settings/ssl:/ssl/ + - ../../../Settings/V1/gateway/adminapi-packaged.conf:/etc/nginx/templates/default.conf.template + depends_on: + - adminapi + + adminapi: + build: + context: ../../../ + dockerfile: api.mssql.Dockerfile + environment: + ADMIN_MSSQL_HOST: db-admin + PATH_BASE: "${ODS_VIRTUAL_NAME:-api}" + TPDM_ENABLED: "${TPDM_ENABLED:-true}" + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_SECURITY_DATASOURCE: db-admin + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + DATABASEENGINE: "SqlServer" + API_MODE: ${API_MODE} + AUTHORITY: ${AUTHORITY} + ISSUER_URL: ${ISSUER_URL} + SIGNING_KEY: ${SIGNING_KEY} + ADMIN_API_VIRTUAL_NAME: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + ODS_API_VERSION: ${ODS_API_VERSION} + ENCRYPT_CONNECTION: "${ENCRYPT_CONNECTION:-false}" + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V1} +# --- + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + API_INTERNAL_URL: ${API_INTERNAL_URL} + AppSettings__DatabaseEngine: SqlServer + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: false + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: false + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: false + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + SQLSERVER_PORT: 1433 + + depends_on: + - db-admin + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin: + build: + context: ../../../Settings/V1/DB-Admin/mssql/ + dockerfile: Dockerfile + environment: + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_SECURITY_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1433:1433 + volumes: + - vol-db-admin-adminapi:/var/lib/mssql/data + restart: always + container_name: ed-fi-db-admin-adminapi + +volumes: + vol-db-admin-adminapi: + driver: local + name: vol-db-admin-adminapi diff --git a/Docker/Compose/mssql/compose-build-dev.yml b/Docker/V1/Compose/mssql/compose-build-dev.yml similarity index 65% rename from Docker/Compose/mssql/compose-build-dev.yml rename to Docker/V1/Compose/mssql/compose-build-dev.yml index 7e76cc3f9..c243bf004 100644 --- a/Docker/Compose/mssql/compose-build-dev.yml +++ b/Docker/V1/Compose/mssql/compose-build-dev.yml @@ -8,7 +8,7 @@ version: "3.8" services: db-ods: build: - context: ../../Settings/DB-Ods/mssql/ + context: ../../../Settings/V1/DB-Ods/mssql/ dockerfile: Dockerfile args: ODS_VERSION: ${MSSQL_ODS_MINIMAL_VERSION:-6.2.982} @@ -31,7 +31,7 @@ services: nginx: build: - context: ../../Settings/gateway/ + context: ../../../Settings/V1/gateway/ dockerfile: Dockerfile environment: ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" @@ -43,7 +43,7 @@ services: restart: always hostname: nginx volumes: - - ../../Settings/ssl:/ssl/ + - ../../../Settings/ssl:/ssl/ depends_on: - adminapi - api @@ -79,9 +79,9 @@ services: # Important to set the context to the root, rather than setting it to the # EdFi.Ods.AdminApi directory, so that the Dockerfile has access to other # C# projects. - context: ../../ + context: ../../../ additional_contexts: - assets: ../../../ + assets: ../../../../ dockerfile: dev.mssql.Dockerfile environment: ADMIN_MSSQL_HOST: db-admin @@ -98,6 +98,35 @@ services: SIGNING_KEY: ${SIGNING_KEY} ADMIN_API_VIRTUAL_NAME: ${ADMIN_API_VIRTUAL_NAME:-adminapi} ODS_API_VERSION: ${ODS_API_VERSION} + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V1} +# --- + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + ADMIN_WAIT_MSSQL_HOSTS: "db-admin " + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: ${ENABLE_CORS:-false} + AppSettings__DatabaseEngine: "SqlServer" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: false + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: false + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: false + Authentication__AllowRegistration: true + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + EnableDockerEnvironment: true + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + depends_on: - db-admin restart: always @@ -110,10 +139,10 @@ services: db-admin: build: - context: ../../ - dockerfile: "db.mssql.admin.Dockerfile" + context: ../../../ + dockerfile: V1/db.mssql.admin.Dockerfile additional_contexts: - assets: ../../../ + assets: ../../../../ environment: SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" ACCEPT_EULA: "Y" diff --git a/Docker/Compose/mssql/env.example b/Docker/V1/Compose/mssql/env.example similarity index 80% rename from Docker/Compose/mssql/env.example rename to Docker/V1/Compose/mssql/env.example index d4a5569df..3408bbacf 100644 --- a/Docker/Compose/mssql/env.example +++ b/Docker/V1/Compose/mssql/env.example @@ -26,5 +26,16 @@ ADMIN_API_HEALTHCHECK_TEST="curl -f http://${ADMIN_API_VIRTUAL_NAME}/health" # ADMIN_API_HEALTHCHECK_TEST=/bin/true # To add a custom health check, consult the documentation at https://docs.docker.com/compose/compose-file/compose-file-v3/#healthcheck +PAGING_OFFSET=0 +PAGING_LIMIT=25 + # Ods Api API_HEALTHCHECK_TEST="curl -f http://localhost/health" + +IPRATELIMITING__ENABLEENDPOINTRATELIMITING=false +IPRATELIMITING__STACKBLOCKEDREQUESTS=false +IPRATELIMITING__REALIPHEADER=X-Real-IP +IPRATELIMITING__CLIENTIDHEADER=X-ClientId +IPRATELIMITING__HTTPSTATUSCODE=429 +IPRATELIMITING__IPWHITELIST=[] +IPRATELIMITING__ENDPOINTWHITELIST=[] \ No newline at end of file diff --git a/Docker/V1/Compose/pgsql/compose-build-binaries.yml b/Docker/V1/Compose/pgsql/compose-build-binaries.yml new file mode 100644 index 000000000..df7f9d32a --- /dev/null +++ b/Docker/V1/Compose/pgsql/compose-build-binaries.yml @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +version: "3.8" + +services: + nginx: + build: + context: ../../../Settings/V1/gateway/ + dockerfile: Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi-packaged + restart: always + hostname: nginx + volumes: + - ../../../Settings/ssl:/ssl/ + - ../../../Settings/V1/gateway/adminapi-packaged.conf:/etc/nginx/templates/default.conf.template + depends_on: + - adminapi + + adminapi: + build: + context: ../../../ + dockerfile: api.pgsql.Dockerfile + args: + ADMIN_API_VERSION: ${ADMIN_API_VERSION:-1.4.2} + environment: + ADMIN_POSTGRES_HOST: db-admin + POSTGRES_PORT: 5432 + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + DATABASEENGINE: "PostgreSql" + API_MODE: ${API_MODE} + AUTHORITY: ${AUTHORITY} + ISSUER_URL: ${ISSUER_URL} + SIGNING_KEY: ${SIGNING_KEY} + ADMIN_API_VIRTUAL_NAME: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + ODS_API_VERSION: ${ODS_API_VERSION} + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V1} + EnableDockerEnvironment: true + SwaggerSettings__EnableSwagger: true + EnableSwagger: true + Authentication__AllowRegistration: true +# --- + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + AppSettings__DatabaseEngine: "PostgreSql" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: false + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: false + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: "${PREVENT_DUPLICATE_APPLICATIONS:-false}" + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + ConnectionStrings__EdFi_Admin: "host=db-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + ConnectionStrings__EdFi_Security: "host=db-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + + depends_on: + - db-admin + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi-packaged + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin: + image: edfialliance/ods-admin-api-db:v1.4.3 + environment: + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + API_MODE: ${API_MODE} + ports: + - "5401:5432" + volumes: + - vol-db-admin-adminapi:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi + +volumes: + vol-db-admin-adminapi: + driver: local + name: vol-db-admin-adminapi diff --git a/Docker/Compose/pgsql/compose-build-dev.yml b/Docker/V1/Compose/pgsql/compose-build-dev.yml similarity index 54% rename from Docker/Compose/pgsql/compose-build-dev.yml rename to Docker/V1/Compose/pgsql/compose-build-dev.yml index 0cf3bd77f..eda3bc541 100644 --- a/Docker/Compose/pgsql/compose-build-dev.yml +++ b/Docker/V1/Compose/pgsql/compose-build-dev.yml @@ -19,7 +19,7 @@ services: nginx: build: - context: ../../Settings/gateway/ + context: ../../../Settings/V1/gateway/ dockerfile: Dockerfile environment: ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" @@ -31,7 +31,7 @@ services: restart: always hostname: nginx volumes: - - ../../Settings/ssl:/ssl/ + - ../../../Settings/ssl:/ssl/ depends_on: - adminapi - api @@ -41,43 +41,31 @@ services: environment: POSTGRES_USER: "${POSTGRES_USER}" POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" - POSTGRES_PORT: "${PGBOUNCER_LISTEN_PORT:-6432}" - ODS_POSTGRES_HOST: pb-ods - ADMIN_POSTGRES_HOST: pb-admin + POSTGRES_PORT: "${POSTGRES_PORT:-5432}" + ODS_POSTGRES_HOST: db-ods + ADMIN_POSTGRES_HOST: db-admin API_MODE: "SharedInstance" ApiSettings__PathBase: "${ODS_VIRTUAL_NAME:-api}" TPDM_ENABLED: "${TPDM_ENABLED:-true}" depends_on: - - pb-ods - - pb-admin + - db-ods + - db-admin restart: always hostname: api container_name: ed-fi-ods-api-adminapi - pb-ods: - image: pgbouncer/pgbouncer@sha256:aa8a38b7b33e5fe70c679053f97a8e55c74d52b00c195f0880845e52b50ce516 #pgbouncer:1.15.0 - environment: - DATABASES: "* = host = db-ods port=5432 user=${POSTGRES_USER} password=${POSTGRES_PASSWORD}" - PGBOUNCER_LISTEN_PORT: "${PGBOUNCER_LISTEN_PORT:-6432}" - ports: - - "5402:${PGBOUNCER_LISTEN_PORT:-6432}" - restart: always - container_name: ed-fi-pb-ods-adminapi - depends_on: - - db-ods - adminapi: build: # Important to set the context to the root, rather than setting it to the # EdFi.Ods.AdminApi directory, so that the Dockerfile has access to other # C# projects. - context: ../../ + context: ../../../ dockerfile: dev.pgsql.Dockerfile additional_contexts: - assets: ../../../ + assets: ../../../../ environment: - ADMIN_POSTGRES_HOST: pb-admin - POSTGRES_PORT: "${PGBOUNCER_LISTEN_PORT:-6432}" + ADMIN_POSTGRES_HOST: db-admin + POSTGRES_PORT: "${POSTGRES_PORT:-5432}" POSTGRES_USER: "${POSTGRES_USER}" POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" DATABASEENGINE: "PostgreSql" @@ -87,8 +75,34 @@ services: SIGNING_KEY: ${SIGNING_KEY} ADMIN_API_VIRTUAL_NAME: ${ADMIN_API_VIRTUAL_NAME:-adminapi} ODS_API_VERSION: ${ODS_API_VERSION} + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V1} + ADMIN_WAIT_POSTGRES_HOSTS: "db-admin " + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + AppSettings__DatabaseEngine: "PostgreSql" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: false + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: false + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + Authentication__AllowRegistration: true + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + ConnectionStrings__EdFi_Admin: "host=db-admin;port=5432;username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + ConnectionStrings__EdFi_Security: "host=db-admin;port=5432;username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + EnableDockerEnvironment: true + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + depends_on: - - pb-admin + - db-admin restart: always hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} container_name: adminapi @@ -99,10 +113,10 @@ services: db-admin: build: - context: ../../ - dockerfile: "db.pgsql.admin.Dockerfile" + context: ../../../ + dockerfile: V1/db.pgsql.admin.Dockerfile additional_contexts: - assets: ../../../ + assets: ../../../../ environment: POSTGRES_USER: "${POSTGRES_USER}" POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" @@ -114,18 +128,6 @@ services: restart: always container_name: ed-fi-db-admin-adminapi - pb-admin: - image: pgbouncer/pgbouncer@sha256:aa8a38b7b33e5fe70c679053f97a8e55c74d52b00c195f0880845e52b50ce516 #pgbouncer:1.15.0 - environment: - DATABASES: "* = host = db-admin port=5432 user=${POSTGRES_USER} password=${POSTGRES_PASSWORD}" - PGBOUNCER_LISTEN_PORT: "${PGBOUNCER_LISTEN_PORT:-6432}" - ports: - - "5401:${PGBOUNCER_LISTEN_PORT:-6432}" - restart: always - container_name: ed-fi-pb-admin-adminapi - depends_on: - - db-admin - volumes: vol-db-admin-adminapi: driver: local diff --git a/Docker/Compose/pgsql/env.example b/Docker/V1/Compose/pgsql/env.example similarity index 74% rename from Docker/Compose/pgsql/env.example rename to Docker/V1/Compose/pgsql/env.example index dd04d0792..d0c3dc855 100644 --- a/Docker/Compose/pgsql/env.example +++ b/Docker/V1/Compose/pgsql/env.example @@ -10,7 +10,7 @@ SIGNING_KEY==1.2.4_git20230717-r5' -USER postgres +RUN apk upgrade --no-cache && apk add --no-cache dos2unix=~7 unzip=~6 openssl=~3 musl=~1 +USER ${POSTGRES_USER} FROM base AS setup @@ -24,7 +26,7 @@ USER root RUN dos2unix /docker-entrypoint-initdb.d/3-run-adminapi-migrations.sh && \ dos2unix /tmp/AdminApiScripts/PgSql/* && \ chmod -R 777 /tmp/AdminApiScripts/PgSql/* -USER postgres +USER ${POSTGRES_USER} EXPOSE 5432 diff --git a/Docker/V2/Compose/mssql/MultiTenant/compose-build-binaries-multi-tenant.yml b/Docker/V2/Compose/mssql/MultiTenant/compose-build-binaries-multi-tenant.yml new file mode 100644 index 000000000..6494dc0ec --- /dev/null +++ b/Docker/V2/Compose/mssql/MultiTenant/compose-build-binaries-multi-tenant.yml @@ -0,0 +1,139 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi-packaged + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../Settings/V2/gateway/default.conf.template:/etc/nginx/templates/default.conf.template + depends_on: + - adminapi + + adminapi: + build: + context: ../../../../ + dockerfile: V2/api.mssql.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + ADMIN_WAIT_MSSQL_HOSTS: "db-admin-tenant1 db-admin-tenant2" + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "SqlServer" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: ${PREVENT_DUPLICATE_APPLICATIONS:-false} + ASPNETCORE_ENVIRONMENT: "multitenantdocker" + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + SQLSERVER_SECURITY_DATASOURCE: db-admin + SQLSERVER_TENANT1_PORT: 1433 + SQLSERVER_TENANT2_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SwaggerSettings__DefaultTenant: ${DEFAULT_TENANT:-tenant1} + Tenants__tenant1__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant1__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant2__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant2__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + entrypoint: ["/bin/sh"] + command: ["-c","/app/run.sh"] + depends_on: + - db-admin-tenant1 + - db-admin-tenant2 + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi-packaged + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin-tenant1: + build: + context: ../../../../Settings/V2/DB-Admin/mssql/ + dockerfile: Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1433:1433 + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 10s + retries: 10 + volumes: + - vol-db-admin-adminapi-tenant1:/var/lib/mssql/data + restart: always + container_name: ed-fi-db-admin-adminapi-tenant1 + + db-admin-tenant2: + build: + context: ../../../../Settings/V2/DB-Admin/mssql/ + dockerfile: Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1434:1433 + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 10s + retries: 10 + volumes: + - vol-db-admin-adminapi-tenant2:/var/lib/mssql/data + restart: always + container_name: ed-fi-db-admin-adminapi-tenant2 + +volumes: + vol-db-admin-adminapi-tenant1: + driver: local + name: vol-db-admin-adminapi-tenant1 + vol-db-admin-adminapi-tenant2: + driver: local + name: vol-db-admin-adminapi-tenant2 diff --git a/Docker/V2/Compose/mssql/MultiTenant/compose-build-dev-multi-tenant.yml b/Docker/V2/Compose/mssql/MultiTenant/compose-build-dev-multi-tenant.yml new file mode 100644 index 000000000..b6a2a9cdf --- /dev/null +++ b/Docker/V2/Compose/mssql/MultiTenant/compose-build-dev-multi-tenant.yml @@ -0,0 +1,153 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ODS_VIRTUAL_NAME: "${ODS_VIRTUAL_NAME:-api}" + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + depends_on: + - adminapi + + adminapi: + build: + # Important to set the context to the root, rather than setting it to the + # EdFi.Ods.AdminApi directory, so that the Dockerfile has access to other + # C# projects. + context: ../../../../ + additional_contexts: + assets: ../../../../../ + args: + ASPNETCORE_ENVIRONMENT: "multitenantdocker" + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + dockerfile: dev.mssql.Dockerfile + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + ADMIN_WAIT_MSSQL_HOSTS: "db-admin-tenant1 db-admin-tenant2" + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "SqlServer" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: ${MULTITENANCY_ENABLED:-true} + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: ${PREVENT_DUPLICATE_APPLICATIONS:-false} + ASPNETCORE_ENVIRONMENT: "multitenantdocker" + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: ${ENABLE_CORS:-false} + Authentication__AllowRegistration: true + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + EnableDockerEnvironment: true + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODs_DATASOURCE: db-admin + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + SQLSERVER_SECURITY_DATASOURCE: db-admin + SQLSERVER_TENANT1_PORT: 1433 + SQLSERVER_TENANT2_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SwaggerSettings__DefaultTenant: ${DEFAULT_TENANT:-tenant2} + Tenants__tenant1__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Admin;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant1__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Security;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant2__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Admin;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant2__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Security;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + entrypoint: ["/bin/sh"] + command: ["-c","/app/run.sh"] + depends_on: + - db-admin-tenant1 + - db-admin-tenant2 + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin-tenant1: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: V2/db.mssql.admin.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1433:1433 + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 10s + retries: 10 + volumes: + - vol-db-admin-adminapi-tenant1:/var/lib/mssql/data + - ../../../../Settings/dev/mssql/adminapi-test-seeddata.sql:/tmp/AdminApiScripts/Admin/MsSql/adminapi-test-seeddata.sql + restart: always + container_name: ed-fi-db-admin-adminapi-tenant1 + + db-admin-tenant2: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: V2/db.mssql.admin.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1434:1433 + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 10s + retries: 10 + volumes: + - vol-db-admin-adminapi-tenant2:/var/lib/mssql/data + - ../../../../Settings/dev/mssql/adminapi-test-seeddata.sql:/tmp/AdminApiScripts/Admin/MsSql/adminapi-test-seeddata.sql + restart: always + container_name: ed-fi-db-admin-adminapi-tenant2 + +volumes: + vol-db-admin-adminapi-tenant1: + driver: local + name: vol-db-admin-adminapi-tenant1 + vol-db-admin-adminapi-tenant2: + driver: local + name: vol-db-admin-adminapi-tenant2 diff --git a/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml b/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml new file mode 100644 index 000000000..b3d76f28a --- /dev/null +++ b/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml @@ -0,0 +1,187 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: IDP.Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + KEYCLOAK_VIRTUAL_NAME: ${KEYCLOAK_VIRTUAL_NAME:-auth} + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi-packaged + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../Settings/V2/gateway/default_idp.conf.template:/etc/nginx/templates/default.conf.template + depends_on: + - adminapi + - idp-keycloak + + adminapi: + build: + context: ../../../../ + dockerfile: api.mssql.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + ADMIN_WAIT_MSSQL_HOSTS: "db-admin-tenant1 db-admin-tenant2" + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "SqlServer" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: ${PREVENT_DUPLICATE_APPLICATIONS:-false} + ASPNETCORE_ENVIRONMENT: "multitenantdocker" + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + SQLSERVER_SECURITY_DATASOURCE: db-admin + SQLSERVER_TENANT1_PORT: 1433 + SQLSERVER_TENANT2_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SwaggerSettings__DefaultTenant: ${DEFAULT_TENANT:-tenant1} + Tenants__tenant1__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant1__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant2__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant2__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + entrypoint: ["/bin/sh"] + command: ["-c","/app/run.sh"] + depends_on: + - db-admin-tenant1 + - db-admin-tenant2 + - idp-keycloak + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi-packaged + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin-tenant1: + build: + context: ../../../../Settings/V2/DB-Admin/mssql/ + dockerfile: Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1433:1433 + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 10s + retries: 10 + volumes: + - vol-db-admin-adminapi-tenant1:/var/lib/mssql/data + restart: always + container_name: ed-fi-db-admin-adminapi-tenant1 + + db-admin-tenant2: + build: + context: ../../../../Settings/V2/DB-Admin/mssql/ + dockerfile: Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1434:1433 + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 10s + retries: 10 + volumes: + - vol-db-admin-adminapi-tenant2:/var/lib/mssql/data + restart: always + container_name: ed-fi-db-admin-adminapi-tenant2 + + idp-keycloak: + image: quay.io/keycloak/keycloak:${KEYCLOAK_IMAGE_TAG:-26.0} + command: start-dev --import-realm + environment: + KC_HOSTNAME: "https://${KEYCLOAK_HOSTNAME:-localhost}/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_HTTP_ENABLED: true + KC_HTTPS_ENABLED: true + KC_HOSTNAME_STRICT: true + KC_HEALTH_ENABLED: ${KEYCLOAK_HEALTH_ENABLED:-true} + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://db-idp-keycloak/${KEYCLOAK_POSTGRES_DB:-keycloak_db} + KC_PROXY_HEADERS: forwarded + PROXY_ADDRESS_FORWARDING: true + KC_DB_USERNAME: ${KEYCLOAK_POSTGRES_USER:-edfi} + KC_DB_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + KC_HTTP_RELATIVE_PATH: "/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_SPI_LOGIN_PROTOCOL_OPENID_CONNECT_LEGACY_IFRAME_CHECK: false + KC_LOG_LEVEL: ${KEYCLOAK_LOG_LEVEL:-OFF} + KC_LOG: console + KC_HTTPS_CERTIFICATE_FILE: /ssl/server.crt + KC_HTTPS_CERTIFICATE_KEY_FILE: /ssl/server.key + ports: + - ${KEYCLOAK_PORT:-28080}:8080 + restart: always + depends_on: + - db-idp-keycloak + hostname: ${KEYCLOAK_VIRTUAL_NAME:-idp_keycloak} + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../KeyCloak/realm-and-users.json:/opt/keycloak/data/import/realm-and-users.json + container_name: ed-fi-idp-keycloak + + db-idp-keycloak: + image: postgres:${KEYCLOAK_DB_IMAGE_TAG:-16.2} + volumes: + - vol-db-idp-keycloak:/var/lib/postgresql/data + environment: + POSTGRES_DB: ${KEYCLOAK_POSTGRES_DB:-keycloak_db} + POSTGRES_USER: ${KEYCLOAK_POSTGRES_USER:-edfi} + POSTGRES_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + container_name: ed-fi-db-idp-keycloak + +volumes: + vol-db-admin-adminapi-tenant1: + driver: local + name: vol-db-admin-adminapi-tenant1 + vol-db-admin-adminapi-tenant2: + driver: local + name: vol-db-admin-adminapi-tenant2 + vol-db-idp-keycloak: + driver: local + name: vol-db-idp-keycloak diff --git a/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-dev-multi-tenant.yml b/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-dev-multi-tenant.yml new file mode 100644 index 000000000..c8fec34f2 --- /dev/null +++ b/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-dev-multi-tenant.yml @@ -0,0 +1,199 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: IDP.Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ODS_VIRTUAL_NAME: "${ODS_VIRTUAL_NAME:-api}" + KEYCLOAK_VIRTUAL_NAME: ${KEYCLOAK_VIRTUAL_NAME:-auth} + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + depends_on: + - adminapi + - idp-keycloak + + adminapi: + build: + # Important to set the context to the root, rather than setting it to the + # EdFi.Ods.AdminApi directory, so that the Dockerfile has access to other + # C# projects. + context: ../../../../ + additional_contexts: + assets: ../../../../../ + args: + ASPNETCORE_ENVIRONMENT: "multitenantdocker" + dockerfile: dev.mssql.Dockerfile + environment: + ADMIN_WAIT_MSSQL_HOSTS: "db-admin-tenant1 db-admin-tenant2" + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "SqlServer" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: ${PREVENT_DUPLICATE_APPLICATIONS:-false} + ASPNETCORE_ENVIRONMENT: "multitenantdocker" + Authentication__AllowRegistration: true + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + EnableDockerEnvironment: true + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + SQLSERVER_SECURITY_DATASOURCE: db-admin + SQLSERVER_TENANT1_PORT: 1433 + SQLSERVER_TENANT2_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SwaggerSettings__DefaultTenant: ${DEFAULT_TENANT:-tenant2} + Tenants__tenant1__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant1__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant2__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant2__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + entrypoint: ["/bin/sh"] + command: ["-c","/app/run.sh"] + depends_on: + - db-admin-tenant1 + - db-admin-tenant2 + - idp-keycloak + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin-tenant1: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: V2/db.mssql.admin.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1433:1433 + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 10s + retries: 10 + volumes: + - vol-db-admin-adminapi-tenant1:/var/lib/mssql/data + - ../../../../Settings/dev/mssql/adminapi-test-seeddata.sql:/tmp/AdminApiScripts/Admin/MsSql/adminapi-test-seeddata.sql + restart: always + container_name: ed-fi-db-admin-adminapi-tenant1 + + db-admin-tenant2: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: V2/db.mssql.admin.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1434:1433 + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 10s + retries: 10 + volumes: + - vol-db-admin-adminapi-tenant2:/var/lib/mssql/data + - ../../../../Settings/dev/mssql/adminapi-test-seeddata.sql:/tmp/AdminApiScripts/Admin/MsSql/adminapi-test-seeddata.sql + restart: always + container_name: ed-fi-db-admin-adminapi-tenant2 + + idp-keycloak: + image: quay.io/keycloak/keycloak:${KEYCLOAK_IMAGE_TAG:-26.0} + command: start-dev --import-realm + environment: + KC_HOSTNAME: "https://${KEYCLOAK_HOSTNAME:-localhost}/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_HTTP_ENABLED: true + KC_HTTPS_ENABLED: true + KC_HOSTNAME_STRICT: true + KC_HEALTH_ENABLED: ${KEYCLOAK_HEALTH_ENABLED:-true} + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://db-idp-keycloak/${KEYCLOAK_POSTGRES_DB:-keycloak_db} + KC_PROXY_HEADERS: forwarded + PROXY_ADDRESS_FORWARDING: true + KC_DB_USERNAME: ${KEYCLOAK_POSTGRES_USER:-edfi} + KC_DB_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + KC_HTTP_RELATIVE_PATH: "/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_SPI_LOGIN_PROTOCOL_OPENID_CONNECT_LEGACY_IFRAME_CHECK: false + KC_LOG_LEVEL: ${KEYCLOAK_LOG_LEVEL:-OFF} + KC_LOG: console + KC_HTTPS_CERTIFICATE_FILE: /ssl/server.crt + KC_HTTPS_CERTIFICATE_KEY_FILE: /ssl/server.key + ports: + - ${KEYCLOAK_PORT:-28080}:8080 + restart: always + depends_on: + - db-idp-keycloak + hostname: ${KEYCLOAK_VIRTUAL_NAME:-idp_keycloak} + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../KeyCloak/realm-and-users.json:/opt/keycloak/data/import/realm-and-users.json + container_name: ed-fi-idp-keycloak + + db-idp-keycloak: + image: postgres:${KEYCLOAK_DB_IMAGE_TAG:-16.2} + volumes: + - vol-db-idp-keycloak:/var/lib/postgresql/data + environment: + POSTGRES_DB: ${KEYCLOAK_POSTGRES_DB:-keycloak_db} + POSTGRES_USER: ${KEYCLOAK_POSTGRES_USER:-edfi} + POSTGRES_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + container_name: ed-fi-db-idp-keycloak + +volumes: + vol-db-admin-adminapi-tenant1: + driver: local + name: vol-db-admin-adminapi-tenant1 + vol-db-admin-adminapi-tenant2: + driver: local + name: vol-db-admin-adminapi-tenant2 + vol-db-idp-keycloak: + driver: local + name: vol-db-idp-keycloak diff --git a/Docker/V2/Compose/mssql/MultiTenant/compose-build-ods-multi-tenant.yml b/Docker/V2/Compose/mssql/MultiTenant/compose-build-ods-multi-tenant.yml new file mode 100644 index 000000000..3aaccf9fa --- /dev/null +++ b/Docker/V2/Compose/mssql/MultiTenant/compose-build-ods-multi-tenant.yml @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + db-ods-tenant1: + build: + context: ../../../../Settings/V2/DB-Ods/mssql/ + dockerfile: Dockerfile + args: + ODS_VERSION: ${MSSQL_ODS_MINIMAL_VERSION:-7.3.478} + TPDM_VERSION: ${MSSQL_TPDM_MINIMAL_VERSION:-7.3.326} + STANDARD_VERSION: ${STANDARD_VERSION:-5.2.0} + EXTENSION_VERSION: ${EXTENSION_VERSION:-1.1.0} + environment: + ACCEPT_EULA: "Y" + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: ${SQLSERVER_PASSWORD:-P@55w0rd} + TPDM_ENABLED: ${TPDM_ENABLED:-true} + MSSQL_PID: ${MSSQL_PID:-Express} + volumes: + - vol-db-ods-tenant1:/var/opt/mssql/data + - vol-db-ods-tenant1:/var/opt/mssql/log + restart: always + container_name: ed-fi-db-ods-tenant1 + hostname: ed-fi-db-ods-tenant1 + healthcheck: + test: /opt/mssql-tools18/bin/sqlcmd -U ${SQLSERVER_USER:-edfi} -P "${SQLSERVER_PASSWORD:-P@55w0rd}" -C -Q "SELECT 1" + start_period: "60s" + retries: 3 + + db-ods-tenant2: + build: + context: ../../../../Settings/V2/DB-Ods/mssql/ + dockerfile: Dockerfile + args: + ODS_VERSION: ${MSSQL_ODS_MINIMAL_VERSION:-7.3.478} + TPDM_VERSION: ${MSSQL_TPDM_MINIMAL_VERSION:-7.3.326} + STANDARD_VERSION: ${STANDARD_VERSION:-5.2.0} + EXTENSION_VERSION: ${EXTENSION_VERSION:-1.1.0} + environment: + ACCEPT_EULA: "Y" + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: ${SQLSERVER_PASSWORD:-P@55w0rd} + TPDM_ENABLED: ${TPDM_ENABLED:-true} + MSSQL_PID: ${MSSQL_PID:-Express} + volumes: + - vol-db-ods-tenant2:/var/opt/mssql/data + - vol-db-ods-tenant2:/var/opt/mssql/log + restart: always + container_name: ed-fi-db-ods-tenant2 + hostname: ed-fi-db-ods-tenant2 + healthcheck: + test: /opt/mssql-tools18/bin/sqlcmd -U ${SQLSERVER_USER:-edfi} -P "${SQLSERVER_PASSWORD:-P@55w0rd}" -C -Q "SELECT 1" + start_period: "60s" + retries: 3 + + api: + image: edfialliance/ods-api-web-api:${TAG}-mssql + environment: + API_HEALTHCHECK_TEST: ${API_HEALTHCHECK_TEST?Please consult env.example to set the API healthcheck test} + ApiSettings__Features:11:IsEnabled: true + ASPNETCORE_ENVIRONMENT: "docker" + ENCRYPT_CONNECTION: "${ENCRYPT_CONNECTION:-false}" + ODS_CONNECTION_STRING_ENCRYPTION_KEY: "${ODS_CONNECTION_STRING_ENCRYPTION_KEY}" + ODS_WAIT_HOSTS: "db-admin-tenant1 db-admin-tenant2" + PATH_BASE: "${ODS_VIRTUAL_NAME:-api}" + SQLSERVER_ADMIN_DATASOURCE: "${SQLSERVER_ADMIN_DATASOURCE:-host.docker.internal}" + SQLSERVER_PASSWORD: ${SQLSERVER_PASSWORD:-P@55w0rd} + SQLSERVER_TENANT1_PORT: 1433 + SQLSERVER_TENANT2_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + TPDM_ENABLED: "${TPDM_ENABLED:-true}" + Tenants__tenant1__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Admin;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant1__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Security;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant2__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Admin;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + Tenants__tenant2__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Security;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + volumes: + - ${LOGS_FOLDER}:/app/logs + depends_on: + - db-ods-tenant1 + - db-ods-tenant2 + - db-admin-tenant1 + - db-admin-tenant2 + restart: always + hostname: api + container_name: ed-fi-ods-api + healthcheck: + test: $$API_HEALTHCHECK_TEST + start_period: "60s" + retries: 3 + +volumes: + vol-db-ods-tenant1: + driver: local + name: vol-nuget-db-ods-tenant1-local + vol-db-ods-tenant2: + driver: local + name: vol-nuget-db-ods-tenant2-local \ No newline at end of file diff --git a/Docker/V2/Compose/mssql/SingleTenant/compose-build-binaries.yml b/Docker/V2/Compose/mssql/SingleTenant/compose-build-binaries.yml new file mode 100644 index 000000000..86fac53f8 --- /dev/null +++ b/Docker/V2/Compose/mssql/SingleTenant/compose-build-binaries.yml @@ -0,0 +1,105 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi-packaged + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../Settings/V2/gateway/default.conf.template:/etc/nginx/templates/default.conf.template + depends_on: + - adminapi + + adminapi: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: api.mssql.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + ADMIN_MSSQL_HOST: db-admin + API_INTERNAL_URL: ${API_INTERNAL_URL} + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: SqlServer + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: "${PREVENT_DUPLICATE_APPLICATIONS:-true}" + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + SQLSERVER_PORT: 1433 + SQLSERVER_SECURITY_DATASOURCE: db-admin + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + depends_on: + - db-admin + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi-packaged + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin: + build: + context: ../../../../Settings/V2/DB-Admin/mssql/ + dockerfile: Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_SECURITY_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1433:1433 + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 10s + retries: 10 + volumes: + - vol-db-admin-adminapi:/var/lib/mssql/data + restart: always + container_name: ed-fi-db-admin-adminapi + +volumes: + vol-db-admin-adminapi: + driver: local + name: vol-db-admin-adminapi diff --git a/Docker/V2/Compose/mssql/SingleTenant/compose-build-dev.yml b/Docker/V2/Compose/mssql/SingleTenant/compose-build-dev.yml new file mode 100644 index 000000000..4af7944c0 --- /dev/null +++ b/Docker/V2/Compose/mssql/SingleTenant/compose-build-dev.yml @@ -0,0 +1,115 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ODS_VIRTUAL_NAME: "${ODS_VIRTUAL_NAME:-api}" + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + depends_on: + - adminapi + + adminapi: + build: + # Important to set the context to the root, rather than setting it to the + # EdFi.Ods.AdminApi directory, so that the Dockerfile has access to other + # C# projects. + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: dev.mssql.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + ADMIN_MSSQL_HOST: db-admin + ADMIN_WAIT_MSSQL_HOSTS: "db-admin " + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: ${ENABLE_CORS:-false} + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "SqlServer" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: ${MULTITENANCY_ENABLED:-false} + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: ${PREVENT_DUPLICATE_APPLICATIONS:-false} + Authentication__AllowRegistration: true + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + EnableDockerEnvironment: true + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PASSWORD: ${SQLSERVER_PASSWORD:-P@55w0rd} + SQLSERVER_PORT: 1433 + SQLSERVER_SECURITY_DATASOURCE: db-admin + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + entrypoint: ["/bin/sh"] + command: ["-c","/app/run.sh"] + depends_on: + - db-admin + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: V2/db.mssql.admin.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1433:1433 + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 10s + retries: 10 + volumes: + - vol-db-admin-adminapi:/var/lib/mssql/data + - ../../../../Settings/dev/mssql/adminapi-test-seeddata.sql:/tmp/AdminApiScripts/Admin/MsSql/adminapi-test-seeddata.sql + restart: always + container_name: ed-fi-db-admin-adminapi + +volumes: + vol-db-admin-adminapi: + driver: local + name: vol-db-admin-adminapi diff --git a/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-binaries.yml b/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-binaries.yml new file mode 100644 index 000000000..10c92c755 --- /dev/null +++ b/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-binaries.yml @@ -0,0 +1,153 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: IDP.Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + KEYCLOAK_VIRTUAL_NAME: ${KEYCLOAK_VIRTUAL_NAME:-auth} + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi-packaged + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../Settings/V2/gateway/default_idp.conf.template:/etc/nginx/templates/default.conf.template + depends_on: + - adminapi + - idp-keycloak + + adminapi: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: api.mssql.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + ADMIN_MSSQL_HOST: db-admin + API_INTERNAL_URL: ${API_INTERNAL_URL} + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: SqlServer + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: "${PREVENT_DUPLICATE_APPLICATIONS:-true}" + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + SQLSERVER_PORT: 1433 + SQLSERVER_SECURITY_DATASOURCE: db-admin + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + depends_on: + - db-admin + - idp-keycloak + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi-packaged + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin: + build: + context: ../../../../Settings/V2/DB-Admin/mssql/ + dockerfile: Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_SECURITY_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1433:1433 + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 10s + retries: 10 + volumes: + - vol-db-admin-adminapi:/var/lib/mssql/data + restart: always + container_name: ed-fi-db-admin-adminapi + + idp-keycloak: + image: quay.io/keycloak/keycloak:${KEYCLOAK_IMAGE_TAG:-26.0} + command: start-dev --import-realm + environment: + KC_HOSTNAME: "https://${KEYCLOAK_HOSTNAME:-localhost}/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_HTTP_ENABLED: true + KC_HTTPS_ENABLED: true + KC_HOSTNAME_STRICT: true + KC_HEALTH_ENABLED: ${KEYCLOAK_HEALTH_ENABLED:-true} + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://db-idp-keycloak/${KEYCLOAK_POSTGRES_DB:-keycloak_db} + KC_PROXY_HEADERS: forwarded + PROXY_ADDRESS_FORWARDING: true + KC_DB_USERNAME: ${KEYCLOAK_POSTGRES_USER:-edfi} + KC_DB_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + KC_HTTP_RELATIVE_PATH: "/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_SPI_LOGIN_PROTOCOL_OPENID_CONNECT_LEGACY_IFRAME_CHECK: false + KC_LOG_LEVEL: ${KEYCLOAK_LOG_LEVEL:-ERROR} + KC_LOG: console + KC_HTTPS_CERTIFICATE_FILE: /ssl/server.crt + KC_HTTPS_CERTIFICATE_KEY_FILE: /ssl/server.key + ports: + - ${KEYCLOAK_PORT:-28080}:8080 + restart: always + depends_on: + - db-idp-keycloak + hostname: ${KEYCLOAK_VIRTUAL_NAME:-idp_keycloak} + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../KeyCloak/realm-and-users.json:/opt/keycloak/data/import/realm-and-users.json + container_name: ed-fi-idp-keycloak + + db-idp-keycloak: + image: postgres:${KEYCLOAK_DB_IMAGE_TAG:-16.2} + volumes: + - vol-db-idp-keycloak:/var/lib/postgresql/data + environment: + POSTGRES_DB: ${KEYCLOAK_POSTGRES_DB:-keycloak_db} + POSTGRES_USER: ${KEYCLOAK_POSTGRES_USER:-edfi} + POSTGRES_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + container_name: ed-fi-db-idp-keycloak + +volumes: + vol-db-admin-adminapi: + driver: local + name: vol-db-admin-adminapi + vol-db-idp-keycloak: + driver: local + name: vol-db-idp-keycloak diff --git a/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-dev.yml b/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-dev.yml new file mode 100644 index 000000000..fcf974c28 --- /dev/null +++ b/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-dev.yml @@ -0,0 +1,163 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: IDP.Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ODS_VIRTUAL_NAME: "${ODS_VIRTUAL_NAME:-api}" + KEYCLOAK_VIRTUAL_NAME: ${KEYCLOAK_VIRTUAL_NAME:-auth} + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + depends_on: + - adminapi + - idp-keycloak + + adminapi: + build: + # Important to set the context to the root, rather than setting it to the + # EdFi.Ods.AdminApi directory, so that the Dockerfile has access to other + # C# projects. + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: dev.mssql.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + ADMIN_MSSQL_HOST: ed-fi-db-admin-adminapi + ADMIN_WAIT_MSSQL_HOSTS: "db-admin " + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "SqlServer" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: "${PREVENT_DUPLICATE_APPLICATIONS:-true}" + Authentication__AllowRegistration: true + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + EnableDockerEnvironment: true + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + SQLSERVER_ADMIN_DATASOURCE: ed-fi-db-admin-adminapi + SQLSERVER_ODS_DATASOURCE: ed-fi-db-admin-adminapi + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + SQLSERVER_PORT: 1433 + SQLSERVER_SECURITY_DATASOURCE: ed-fi-db-admin-adminapi + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + entrypoint: ["/bin/sh"] + command: ["-c","/app/run.sh"] + depends_on: + - db-admin + - idp-keycloak + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + idp-keycloak: + image: quay.io/keycloak/keycloak:${KEYCLOAK_IMAGE_TAG:-26.0} + command: start-dev --import-realm + environment: + KC_HOSTNAME: "https://${KEYCLOAK_HOSTNAME:-localhost}/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_HTTP_ENABLED: true + KC_HTTPS_ENABLED: true + KC_HOSTNAME_STRICT: true + KC_HEALTH_ENABLED: ${KEYCLOAK_HEALTH_ENABLED:-true} + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://db-idp-keycloak/${KEYCLOAK_POSTGRES_DB:-keycloak_db} + KC_PROXY_HEADERS: forwarded + PROXY_ADDRESS_FORWARDING: true + KC_DB_USERNAME: ${KEYCLOAK_POSTGRES_USER:-edfi} + KC_DB_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + KC_HTTP_RELATIVE_PATH: "/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_SPI_LOGIN_PROTOCOL_OPENID_CONNECT_LEGACY_IFRAME_CHECK: false + KC_LOG_LEVEL: ${KEYCLOAK_LOG_LEVEL:-OFF} + KC_LOG: console + KC_HTTPS_CERTIFICATE_FILE: /ssl/server.crt + KC_HTTPS_CERTIFICATE_KEY_FILE: /ssl/server.key + ports: + - ${KEYCLOAK_PORT:-28080}:8080 + restart: always + depends_on: + - db-idp-keycloak + hostname: ${KEYCLOAK_VIRTUAL_NAME:-idp_keycloak} + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../KeyCloak/realm-and-users.json:/opt/keycloak/data/import/realm-and-users.json + container_name: ed-fi-idp-keycloak + + db-idp-keycloak: + image: postgres:${KEYCLOAK_DB_IMAGE_TAG:-16.2} + volumes: + - vol-db-idp-keycloak:/var/lib/postgresql/data + environment: + POSTGRES_DB: ${KEYCLOAK_POSTGRES_DB:-keycloak_db} + POSTGRES_USER: ${KEYCLOAK_POSTGRES_USER:-edfi} + POSTGRES_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + container_name: ed-fi-db-idp-keycloak + + db-admin: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: V2/db.mssql.admin.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + SA_PASSWORD: "${SA_PASSWORD:-P@55w0rd}" + ACCEPT_EULA: "Y" + SQLSERVER_ADMIN_DATASOURCE: db-admin + SQLSERVER_ODS_DATASOURCE: db-admin + SQLSERVER_PORT: 1433 + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: "${SQLSERVER_PASSWORD:-P@55w0rd}" + ports: + - 1433:1433 + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 10s + retries: 10 + volumes: + - vol-db-admin-adminapi:/var/lib/mssql/data + - ../../../../Settings/dev/mssql/adminapi-test-seeddata.sql:/tmp/AdminApiScripts/Admin/MsSql/adminapi-test-seeddata.sql + restart: always + container_name: ed-fi-db-admin-adminapi + +volumes: + vol-db-admin-adminapi: + driver: local + name: vol-db-admin-adminapi + vol-db-idp-keycloak: + driver: local + name: vol-db-idp-keycloak diff --git a/Docker/V2/Compose/mssql/SingleTenant/compose-build-ods.yml b/Docker/V2/Compose/mssql/SingleTenant/compose-build-ods.yml new file mode 100644 index 000000000..a77ac80e6 --- /dev/null +++ b/Docker/V2/Compose/mssql/SingleTenant/compose-build-ods.yml @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + db-ods: + build: + context: ../../../../Settings/V2/DB-Ods/mssql/ + dockerfile: Dockerfile + args: + ODS_VERSION: ${MSSQL_ODS_MINIMAL_VERSION:-7.3.478} + TPDM_VERSION: ${MSSQL_TPDM_MINIMAL_VERSION:-7.3.326} + STANDARD_VERSION: ${STANDARD_VERSION:-5.2.0} + EXTENSION_VERSION: ${EXTENSION_VERSION:-1.1.0} + environment: + ACCEPT_EULA: "Y" + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + SQLSERVER_PASSWORD: ${SQLSERVER_PASSWORD:-P@55w0rd} + TPDM_ENABLED: ${TPDM_ENABLED:-true} + MSSQL_PID: ${MSSQL_PID:-Express} + volumes: + - vol-db-ods:/var/opt/mssql/data + - vol-db-ods:/var/opt/mssql/log + restart: always + container_name: ed-fi-db-ods + hostname: ed-fi-db-ods + healthcheck: + test: /opt/mssql-tools18/bin/sqlcmd -U ${SQLSERVER_USER:-edfi} -P "${SQLSERVER_PASSWORD:-P@55w0rd}" -C -Q "SELECT 1" + start_period: "60s" + retries: 3 + + api: + image: edfialliance/ods-api-web-api:${TAG}-mssql + environment: + API_HEALTHCHECK_TEST: ${API_HEALTHCHECK_TEST?Please consult env.example to set the API healthcheck test} + ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" + ENCRYPT_CONNECTION: "${ENCRYPT_CONNECTION:-false}" + ODS_CONNECTION_STRING_ENCRYPTION_KEY: "${ODS_CONNECTION_STRING_ENCRYPTION_KEY}" + PATH_BASE: "${ODS_VIRTUAL_NAME:-api}" + SQLSERVER_ADMIN_DATASOURCE: "${SQLSERVER_ADMIN_DATASOURCE:-db-admin}" + SQLSERVER_ODS_DATASOURCE: "${SQLSERVER_ODS_DATASOURCE:-ed-fi-db-ods}" + SQLSERVER_PASSWORD: ${SQLSERVER_PASSWORD:-P@55w0rd} + SQLSERVER_USER: ${SQLSERVER_USER:-edfi} + TPDM_ENABLED: "${TPDM_ENABLED:-true}" + volumes: + - ${LOGS_FOLDER}:/app/logs + depends_on: + - db-ods + - db-admin + restart: always + hostname: api + container_name: ed-fi-ods-api + healthcheck: + test: $$API_HEALTHCHECK_TEST + start_period: "60s" + retries: 3 + +volumes: + vol-db-ods: + driver: local + name: vol-nuget-db-ods-local \ No newline at end of file diff --git a/Docker/V2/Compose/mssql/env-idp.example b/Docker/V2/Compose/mssql/env-idp.example new file mode 100644 index 000000000..1f88b46b2 --- /dev/null +++ b/Docker/V2/Compose/mssql/env-idp.example @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + + +ADMIN_API_VIRTUAL_NAME=adminapi +ADMIN_API_VERSION= 2.2.2-alpha.0.78 + +# For Authentication +AUTHORITY=http://localhost/${ADMIN_API_VIRTUAL_NAME} +ISSUER_URL=https://localhost/${ADMIN_API_VIRTUAL_NAME} +SIGNING_KEY= + +# For Ed-Fi ODS / API +EDFI_API_DISCOVERY_URL=https://api.ed-fi.org/v7.2/api/ + +ENABLE_ADMIN_CONSOLE=true +USE_SELF_CONTAINED_AUTH=false +# OIDC_AUTHORITY=http://ed-fi-idp-keycloak:8080/auth/realms/edfi-admin-console +OIDC_REQUIRE_METADATA=false +OIDC_ENABLE_SERVER_CERTIFICATE=true + + +PAGING_OFFSET=0 +PAGING_LIMIT=25 +# For MSSQL only +SQLSERVER_USER=edfi +SQLSERVER_PASSWORD=P@55w0rd +DATABASE_ENGINE_FOLDER=mssql +# For Multitenant +DEFAULT_TENANT=tenant1 + +# The following needs to be set to specify a health check test for Admin api. +# RECOMMENDED: To use the default internal Admin Api health check endpoint, set the variable as follows: +ADMIN_API_HEALTHCHECK_TEST="wget -nv -t1 --spider http://${ADMIN_API_VIRTUAL_NAME}/health || exit 1" + +# To disable the health check, remove the above and instead set the variable as follows: +# ADMIN_API_HEALTHCHECK_TEST=/bin/true +# To add a custom health check, consult the documentation at https://docs.docker.com/compose/compose-file/compose-file-v3/#healthcheck + + +# IdP db keycloak +KEYCLOAK_DB_IMAGE_TAG=16.2 +KEYCLOAK_POSTGRES_DB=keycloak_db +KEYCLOAK_POSTGRES_USER=edfi +KEYCLOAK_POSTGRES_PASSWORD=P@55w0rd +# IdP keycloak +KEYCLOAK_IMAGE_TAG=26.0 +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_PORT=28080 +KEYCLOAK_VIRTUAL_NAME=keycloak +KEYCLOAK_HOSTNAME= localhost +KEYCLOAK_HOSTNAME_PORT=443 +KEYCLOAK_HOSTNAME_STRICT_BACKCHANNEL=false +KEYCLOAK_HTTP_ENABLED=true +KEYCLOAK_HOSTNAME_STRICT_HTTPS=true +KEYCLOAK_HEALTH_ENABLED=true +KEYCLOAK_ADMIN_CONSOLE_REALM=edfi-admin-console + + +# Ods Api +TAG=7.3 +API_HEALTHCHECK_TEST="curl -f http://localhost/health" +ODS_CONNECTION_STRING_ENCRYPTION_KEY= diff --git a/Docker/V2/Compose/mssql/env.example b/Docker/V2/Compose/mssql/env.example new file mode 100644 index 000000000..b47f06b31 --- /dev/null +++ b/Docker/V2/Compose/mssql/env.example @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + + +ADMIN_API_VIRTUAL_NAME=adminapi +ADMIN_API_VERSION= 2.2.2-alpha.0.78 + +# For Authentication +AUTHORITY=http://localhost/${ADMIN_API_VIRTUAL_NAME} +ISSUER_URL=https://localhost/${ADMIN_API_VIRTUAL_NAME} +SIGNING_KEY= + +# For Ed-Fi ODS / API +EDFI_API_DISCOVERY_URL=https://localhost/api/ + +PAGING_OFFSET=0 +PAGING_LIMIT=25 +# For MSSQL only +SQLSERVER_USER=edfi +SQLSERVER_PASSWORD=P@55w0rd +DATABASE_ENGINE_FOLDER=mssql +# For Multitenant +DEFAULT_TENANT=tenant1 + +# The following needs to be set to specify a health check test for Admin api. +# RECOMMENDED: To use the default internal Admin Api health check endpoint, set the variable as follows: +ADMIN_API_HEALTHCHECK_TEST="wget -nv -t1 --spider http://${ADMIN_API_VIRTUAL_NAME}/health || exit 1" + +# To disable the health check, remove the above and instead set the variable as follows: +# ADMIN_API_HEALTHCHECK_TEST=/bin/true +# To add a custom health check, consult the documentation at https://docs.docker.com/compose/compose-file/compose-file-v3/#healthcheck + +# IdP db keycloak +KEYCLOAK_DB_IMAGE_TAG=16.2 +KEYCLOAK_POSTGRES_DB=keycloak_db +KEYCLOAK_POSTGRES_USER=edfi +KEYCLOAK_POSTGRES_PASSWORD=P@55w0rd +# IdP keycloak +KEYCLOAK_IMAGE_TAG=26.0 +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_PORT=28080 +KEYCLOAK_VIRTUAL_NAME=keycloak +KEYCLOAK_HOSTNAME= localhost +KEYCLOAK_HOSTNAME_PORT=443 +KEYCLOAK_HOSTNAME_STRICT_BACKCHANNEL=false +KEYCLOAK_HTTP_ENABLED=true +KEYCLOAK_HOSTNAME_STRICT_HTTPS=true +KEYCLOAK_HEALTH_ENABLED=true +KEYCLOAK_ADMIN_CONSOLE_REALM=edfi-admin-console + + +# Ods Api +TAG=7.3 +API_HEALTHCHECK_TEST="curl -f http://localhost/health" +ODS_CONNECTION_STRING_ENCRYPTION_KEY= diff --git a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-binaries-multi-tenant.yml b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-binaries-multi-tenant.yml new file mode 100644 index 000000000..f82c2a5f3 --- /dev/null +++ b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-binaries-multi-tenant.yml @@ -0,0 +1,114 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi-packaged + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../Settings/V2/gateway/default.conf.template:/etc/nginx/templates/default.conf.template + depends_on: + - adminapi + + adminapi: + build: + context: ../../../../ + dockerfile: api.pgsql.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + ADMIN_WAIT_POSTGRES_HOSTS: "db-admin-tenant1 db-admin-tenant2 " + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "PostgreSql" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: "${PREVENT_DUPLICATE_APPLICATIONS:-false}" + ASPNETCORE_ENVIRONMENT: "multitenantdocker" + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_PORT: 5432 + POSTGRES_USER: "${POSTGRES_USER}" + Tenants__tenant1__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + Tenants__tenant1__ConnectionStrings__EdFi_Security: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + Tenants__tenant2__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + Tenants__tenant2__ConnectionStrings__EdFi_Security: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + depends_on: + - db-admin-tenant1 + - db-admin-tenant2 + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi-packaged + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin-tenant1: + image: edfialliance/ods-admin-api-db:pre + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + ports: + - "5401:5432" + volumes: + - vol-db-admin-adminapi-tenant1:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi-tenant1 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + start_period: "60s" + retries: 3 + + db-admin-tenant2: + image: edfialliance/ods-admin-api-db:pre + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + ports: + - "5402:5432" + volumes: + - vol-db-admin-adminapi-tenant2:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi-tenant2 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + start_period: "60s" + retries: 3 + +volumes: + vol-db-admin-adminapi-tenant1: + driver: local + name: vol-db-admin-adminapi-tenant1 + vol-db-admin-adminapi-tenant2: + driver: local + name: vol-db-admin-adminapi-tenant2 diff --git a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-dev-multi-tenant.yml b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-dev-multi-tenant.yml new file mode 100644 index 000000000..b92449799 --- /dev/null +++ b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-dev-multi-tenant.yml @@ -0,0 +1,127 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ODS_VIRTUAL_NAME: "${ODS_VIRTUAL_NAME:-api}" + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + depends_on: + - adminapi + + adminapi: + build: + # Important to set the context to the root, rather than setting it to the + # EdFi.Ods.AdminApi directory, so that the Dockerfile has access to other + # C# projects. + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: dev.pgsql.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + ADMIN_WAIT_POSTGRES_HOSTS: "db-admin-tenant1 db-admin-tenant2 " + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "PostgreSql" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: "${PREVENT_DUPLICATE_APPLICATIONS:-false}" + ASPNETCORE_ENVIRONMENT: "multitenantdocker" + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + Authentication__AllowRegistration: true + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + EnableDockerEnvironment: true + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_PORT: "${POSTGRES_PORT:-5432}" + POSTGRES_USER: "${POSTGRES_USER}" + Tenants__tenant1__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant1;port=${POSTGRES_PORT:-5432};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + Tenants__tenant1__ConnectionStrings__EdFi_Security: "host=db-admin-tenant1;port=${POSTGRES_PORT:-5432};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + Tenants__tenant2__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant2;port=${POSTGRES_PORT:-5432};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + Tenants__tenant2__ConnectionStrings__EdFi_Security: "host=db-admin-tenant2;port=${POSTGRES_PORT:-5432};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + entrypoint: ["/bin/sh"] + command: ["-c","/app/run.sh"] + depends_on: + - db-admin-tenant1 + - db-admin-tenant2 + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin-tenant1: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: V2/db.pgsql.admin.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + ports: + - "5401:5432" + volumes: + - vol-db-admin-adminapi-tenant1:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi-tenant1 + + db-admin-tenant2: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: V2/db.pgsql.admin.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + ports: + - "5402:5432" + volumes: + - vol-db-admin-adminapi-tenant2:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi-tenant2 + +volumes: + vol-db-admin-adminapi-tenant1: + driver: local + name: vol-db-admin-adminapi-tenant1 + vol-db-admin-adminapi-tenant2: + driver: local + name: vol-db-admin-adminapi-tenant2 diff --git a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml new file mode 100644 index 000000000..3df35e8cb --- /dev/null +++ b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml @@ -0,0 +1,161 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: IDP.Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + KEYCLOAK_VIRTUAL_NAME: ${KEYCLOAK_VIRTUAL_NAME:-auth} + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi-packaged + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + depends_on: + - adminapi + - idp-keycloak + + adminapi: + build: + context: ../../../../ + dockerfile: api.pgsql.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + ADMIN_WAIT_POSTGRES_HOSTS: "db-admin-tenant1 db-admin-tenant2 " + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "PostgreSql" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: "${PREVENT_DUPLICATE_APPLICATIONS:-false}" + ASPNETCORE_ENVIRONMENT: "multitenantdocker" + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_PORT: 5432 + POSTGRES_USER: "${POSTGRES_USER}" + Tenants__tenant1__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + Tenants__tenant1__ConnectionStrings__EdFi_Security: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + Tenants__tenant2__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + Tenants__tenant2__ConnectionStrings__EdFi_Security: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + depends_on: + - db-admin-tenant1 + - db-admin-tenant2 + - idp-keycloak + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi-packaged + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin-tenant1: + image: edfialliance/ods-admin-api-db:pre + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + ports: + - "5401:5432" + volumes: + - vol-db-admin-adminapi-tenant1:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi-tenant1 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + start_period: "60s" + retries: 3 + + db-admin-tenant2: + image: edfialliance/ods-admin-api-db:pre + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + ports: + - "5402:5432" + volumes: + - vol-db-admin-adminapi-tenant2:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi-tenant2 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + start_period: "60s" + retries: 3 + + idp-keycloak: + image: quay.io/keycloak/keycloak:${KEYCLOAK_IMAGE_TAG:-26.0} + command: start-dev --import-realm + environment: + KC_HOSTNAME: "https://${KEYCLOAK_HOSTNAME:-localhost}/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_HTTP_ENABLED: true + KC_HTTPS_ENABLED: true + KC_HOSTNAME_STRICT: true + KC_HEALTH_ENABLED: ${KEYCLOAK_HEALTH_ENABLED:-true} + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://db-idp-keycloak/${KEYCLOAK_POSTGRES_DB:-keycloak_db} + KC_PROXY_HEADERS: forwarded + PROXY_ADDRESS_FORWARDING: true + KC_DB_USERNAME: ${KEYCLOAK_POSTGRES_USER:-edfi} + KC_DB_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + KC_HTTP_RELATIVE_PATH: "/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_SPI_LOGIN_PROTOCOL_OPENID_CONNECT_LEGACY_IFRAME_CHECK: false + KC_LOG_LEVEL: ${KEYCLOAK_LOG_LEVEL:-OFF} + KC_LOG: console + KC_HTTPS_CERTIFICATE_FILE: /ssl/server.crt + KC_HTTPS_CERTIFICATE_KEY_FILE: /ssl/server.key + ports: + - ${KEYCLOAK_PORT:-28080}:8080 + restart: always + depends_on: + - db-idp-keycloak + hostname: ${KEYCLOAK_VIRTUAL_NAME:-idp_keycloak} + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../KeyCloak/realm-and-users.json:/opt/keycloak/data/import/realm-and-users.json + container_name: ed-fi-idp-keycloak + + db-idp-keycloak: + image: postgres:${KEYCLOAK_DB_IMAGE_TAG:-16.2} + volumes: + - vol-db-idp-keycloak:/var/lib/postgresql/data + environment: + POSTGRES_DB: ${KEYCLOAK_POSTGRES_DB:-keycloak_db} + POSTGRES_USER: ${KEYCLOAK_POSTGRES_USER:-edfi} + POSTGRES_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + container_name: ed-fi-db-idp-keycloak + +volumes: + vol-db-admin-adminapi-tenant1: + driver: local + name: vol-db-admin-adminapi-tenant1 + vol-db-admin-adminapi-tenant2: + driver: local + name: vol-db-admin-adminapi-tenant2 + vol-db-idp-keycloak: + driver: local + name: vol-db-idp-keycloak diff --git a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-dev-multi-tenant.yml b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-dev-multi-tenant.yml new file mode 100644 index 000000000..3e8896a92 --- /dev/null +++ b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-dev-multi-tenant.yml @@ -0,0 +1,175 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: IDP.Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ODS_VIRTUAL_NAME: "${ODS_VIRTUAL_NAME:-api}" + KEYCLOAK_VIRTUAL_NAME: ${KEYCLOAK_VIRTUAL_NAME:-auth} + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + depends_on: + - adminapi + - idp-keycloak + + adminapi: + build: + # Important to set the context to the root, rather than setting it to the + # EdFi.Ods.AdminApi directory, so that the Dockerfile has access to other + # C# projects. + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: dev.pgsql.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + ADMIN_WAIT_POSTGRES_HOSTS: "db-admin-tenant1 db-admin-tenant2 " + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "PostgreSql" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: "${PREVENT_DUPLICATE_APPLICATIONS:-false}" + ASPNETCORE_ENVIRONMENT: "multitenantdocker" + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + Authentication__AllowRegistration: true + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + EnableDockerEnvironment: true + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_PORT: 5432 + POSTGRES_USER: "${POSTGRES_USER}" + Tenants__tenant1__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + Tenants__tenant1__ConnectionStrings__EdFi_Security: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + Tenants__tenant2__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + Tenants__tenant2__ConnectionStrings__EdFi_Security: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + entrypoint: ["/bin/sh"] + command: ["-c","/app/run.sh"] + depends_on: + - db-admin-tenant1 + - db-admin-tenant2 + - idp-keycloak + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin-tenant1: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: V2/db.pgsql.admin.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + ports: + - "5401:5432" + volumes: + - vol-db-admin-adminapi-tenant1:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi-tenant1 + + db-admin-tenant2: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: V2/db.pgsql.admin.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + ports: + - "5402:5432" + volumes: + - vol-db-admin-adminapi-tenant2:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi-tenant2 + + idp-keycloak: + image: quay.io/keycloak/keycloak:${KEYCLOAK_IMAGE_TAG:-26.0} + command: start-dev --import-realm + environment: + KC_HOSTNAME: "https://${KEYCLOAK_HOSTNAME:-localhost}/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_HTTP_ENABLED: true + KC_HTTPS_ENABLED: true + KC_HOSTNAME_STRICT: true + KC_HEALTH_ENABLED: ${KEYCLOAK_HEALTH_ENABLED:-true} + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://db-idp-keycloak/${KEYCLOAK_POSTGRES_DB:-keycloak_db} + KC_PROXY_HEADERS: forwarded + PROXY_ADDRESS_FORWARDING: true + KC_DB_USERNAME: ${KEYCLOAK_POSTGRES_USER:-edfi} + KC_DB_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + KC_HTTP_RELATIVE_PATH: "/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_SPI_LOGIN_PROTOCOL_OPENID_CONNECT_LEGACY_IFRAME_CHECK: false + KC_LOG_LEVEL: ${KEYCLOAK_LOG_LEVEL:-OFF} + KC_LOG: console + KC_HTTPS_CERTIFICATE_FILE: /ssl/server.crt + KC_HTTPS_CERTIFICATE_KEY_FILE: /ssl/server.key + ports: + - ${KEYCLOAK_PORT:-28080}:8080 + restart: always + depends_on: + - db-idp-keycloak + hostname: ${KEYCLOAK_VIRTUAL_NAME:-idp_keycloak} + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../KeyCloak/realm-and-users.json:/opt/keycloak/data/import/realm-and-users.json + container_name: ed-fi-idp-keycloak + + db-idp-keycloak: + image: postgres:${KEYCLOAK_DB_IMAGE_TAG:-16.2} + volumes: + - vol-db-idp-keycloak:/var/lib/postgresql/data + environment: + POSTGRES_DB: ${KEYCLOAK_POSTGRES_DB:-keycloak_db} + POSTGRES_USER: ${KEYCLOAK_POSTGRES_USER:-edfi} + POSTGRES_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + container_name: ed-fi-db-idp-keycloak + +volumes: + vol-db-admin-adminapi-tenant1: + driver: local + name: vol-db-admin-adminapi-tenant1 + vol-db-admin-adminapi-tenant2: + driver: local + name: vol-db-admin-adminapi-tenant2 + vol-db-idp-keycloak: + driver: local + name: vol-db-idp-keycloak diff --git a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-ods-multi-tenant.yml b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-ods-multi-tenant.yml new file mode 100644 index 000000000..e2e6846f4 --- /dev/null +++ b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-ods-multi-tenant.yml @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + db-ods-tenant1: + image: edfialliance/ods-api-db-ods-minimal:${TAG} + environment: + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + TPDM_ENABLED: "${TPDM_ENABLED:-true}" + volumes: + - vol-db-ods-tenant1:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-ods-tenant1 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + start_period: "60s" + retries: 3 + + db-ods-tenant2: + image: edfialliance/ods-api-db-ods-minimal:${TAG} + environment: + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + TPDM_ENABLED: "${TPDM_ENABLED:-true}" + volumes: + - vol-db-ods-tenant2:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-ods-tenant2 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + start_period: "60s" + retries: 3 + + api: + image: edfialliance/ods-api-web-api:${TAG} + environment: + ADMIN_POSTGRES_HOST: db-admin-tenant1 + API_HEALTHCHECK_TEST: ${API_HEALTHCHECK_TEST?Please consult env.example to set the API healthcheck test} + ApiSettings__Features:11:IsEnabled: true + ASPNETCORE_ENVIRONMENT: "docker" + ENCRYPT_CONNECTION: "${ENCRYPT_CONNECTION:-false}" + NPG_API_MAX_POOL_SIZE_ADMIN: "${NPG_API_MAX_POOL_SIZE_ADMIN}" + NPG_API_MAX_POOL_SIZE_MASTER: "${NPG_API_MAX_POOL_SIZE_MASTER}" + NPG_API_MAX_POOL_SIZE_ODS: "${NPG_API_MAX_POOL_SIZE_ODS}" + NPG_API_MAX_POOL_SIZE_SECURITY: "${NPG_API_MAX_POOL_SIZE_SECURITY}" + NPG_POOLING_ENABLED: "${NPG_POOLING_ENABLED:-false}" + ODS_CONNECTION_STRING_ENCRYPTION_KEY: "${ODS_CONNECTION_STRING_ENCRYPTION_KEY}" + ODS_WAIT_POSTGRES_HOSTS: "db-ods-tenant1 db-ods-tenant2 " + PATH_BASE: "${ODS_VIRTUAL_NAME:-api}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_PORT: "${POSTGRES_PORT:-5432}" + POSTGRES_USER: "${POSTGRES_USER}" + TPDM_ENABLED: "${TPDM_ENABLED:-true}" + Tenants__tenant1__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + Tenants__tenant1__ConnectionStrings__EdFi_Security: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + Tenants__tenant2__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + Tenants__tenant2__ConnectionStrings__EdFi_Security: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + volumes: + - ${LOGS_FOLDER}:/app/logs + depends_on: + - db-ods-tenant1 + - db-ods-tenant2 + - db-admin-tenant1 + - db-admin-tenant2 + restart: always + hostname: api + container_name: ed-fi-ods-api + healthcheck: + test: $$API_HEALTHCHECK_TEST + start_period: "60s" + retries: 3 + +volumes: + vol-db-ods-tenant1: + driver: local + name: vol-db-ods-tenant1 + vol-db-ods-tenant2: + driver: local + name: vol-db-ods-tenant2 \ No newline at end of file diff --git a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-binaries.yml b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-binaries.yml new file mode 100644 index 000000000..9331f66c0 --- /dev/null +++ b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-binaries.yml @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi-packaged + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../Settings/V2/gateway/default.conf.template:/etc/nginx/templates/default.conf.template + depends_on: + - adminapi + + adminapi: + build: + context: ../../../../ + dockerfile: api.pgsql.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + ADMIN_POSTGRES_HOST: db-admin + API_INTERNAL_URL: ${API_INTERNAL_URL} + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "PostgreSql" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: "${PREVENT_DUPLICATE_APPLICATIONS:-false}" + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + ConnectionStrings__EdFi_Admin: "host=db-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + ConnectionStrings__EdFi_Security: "host=db-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_PORT: 5432 + POSTGRES_USER: "${POSTGRES_USER}" + depends_on: + - db-admin + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi-packaged + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin: + image: edfialliance/ods-admin-api-db:pre + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + ports: + - "5401:5432" + volumes: + - vol-db-admin-adminapi:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi + +volumes: + vol-db-admin-adminapi: + driver: local + name: vol-db-admin-adminapi diff --git a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-dev.yml b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-dev.yml new file mode 100644 index 000000000..5dd09d27d --- /dev/null +++ b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-dev.yml @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ODS_VIRTUAL_NAME: "${ODS_VIRTUAL_NAME:-api}" + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + depends_on: + - adminapi + + adminapi: + build: + # Important to set the context to the root, rather than setting it to the + # EdFi.Ods.AdminApi directory, so that the Dockerfile has access to other + # C# projects. + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: dev.pgsql.Dockerfile + environment: + ADMIN_POSTGRES_HOST: db-admin + ADMIN_WAIT_POSTGRES_HOSTS: "db-admin " + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "PostgreSql" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + Authentication__AllowRegistration: true + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + ConnectionStrings__EdFi_Admin: "host=db-admin;port=${POSTGRES_PORT:-5432};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + ConnectionStrings__EdFi_Security: "host=db-admin;port=${POSTGRES_PORT:-5432};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + EnableDockerEnvironment: true + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_PORT: "${POSTGRES_PORT:-5432}" + POSTGRES_USER: "${POSTGRES_USER}" + entrypoint: ["/bin/sh"] + command: ["-c","/app/run.sh"] + depends_on: + - db-admin + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: V2/db.pgsql.admin.Dockerfile + environment: + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_PORT: "${POSTGRES_PORT:-5432}" + ports: + - "5880:5432" + volumes: + - vol-db-admin-adminapi:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi + +volumes: + vol-db-admin-adminapi: + driver: local + name: vol-db-admin-adminapi diff --git a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-binaries.yml b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-binaries.yml new file mode 100644 index 000000000..93782d426 --- /dev/null +++ b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-binaries.yml @@ -0,0 +1,134 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: IDP.Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + KEYCLOAK_VIRTUAL_NAME: ${KEYCLOAK_VIRTUAL_NAME:-auth} + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi-packaged + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../Settings/V2/gateway/default_idp.conf.template:/etc/nginx/templates/default.conf.template + depends_on: + - adminapi + - idp-keycloak + + adminapi: + build: + context: ../../../../ + dockerfile: api.pgsql.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + ADMIN_POSTGRES_HOST: db-admin + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + API_INTERNAL_URL: ${API_INTERNAL_URL} + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "PostgreSql" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: "${PREVENT_DUPLICATE_APPLICATIONS:-false}" + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + ConnectionStrings__EdFi_Admin: "host=pb-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + ConnectionStrings__EdFi_Security: "host=pb-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_PORT: 5432 + POSTGRES_USER: "${POSTGRES_USER}" + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi-packaged + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin: + image: edfialliance/ods-admin-api-db:pre + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + ports: + - "5401:5432" + volumes: + - vol-db-admin-adminapi:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi + + idp-keycloak: + image: quay.io/keycloak/keycloak:${KEYCLOAK_IMAGE_TAG:-26.0} + command: start-dev --import-realm + environment: + KC_HOSTNAME: "https://${KEYCLOAK_HOSTNAME:-localhost}/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_HTTP_ENABLED: true + KC_HTTPS_ENABLED: true + KC_HOSTNAME_STRICT: true + KC_HEALTH_ENABLED: ${KEYCLOAK_HEALTH_ENABLED:-true} + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://db-idp-keycloak/${KEYCLOAK_POSTGRES_DB:-keycloak_db} + KC_PROXY_HEADERS: forwarded + PROXY_ADDRESS_FORWARDING: true + KC_DB_USERNAME: ${KEYCLOAK_POSTGRES_USER:-edfi} + KC_DB_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + KC_HTTP_RELATIVE_PATH: "/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_SPI_LOGIN_PROTOCOL_OPENID_CONNECT_LEGACY_IFRAME_CHECK: false + KC_LOG_LEVEL: ${KEYCLOAK_LOG_LEVEL:-OFF} + KC_LOG: console + KC_HTTPS_CERTIFICATE_FILE: /ssl/server.crt + KC_HTTPS_CERTIFICATE_KEY_FILE: /ssl/server.key + ports: + - ${KEYCLOAK_PORT:-28080}:8080 + restart: always + depends_on: + - db-idp-keycloak + hostname: ${KEYCLOAK_VIRTUAL_NAME:-idp_keycloak} + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../KeyCloak/realm-and-users.json:/opt/keycloak/data/import/realm-and-users.json + container_name: ed-fi-idp-keycloak + + db-idp-keycloak: + image: postgres:${KEYCLOAK_DB_IMAGE_TAG:-16.2} + volumes: + - vol-db-idp-keycloak:/var/lib/postgresql/data + environment: + POSTGRES_DB: ${KEYCLOAK_POSTGRES_DB:-keycloak_db} + POSTGRES_USER: ${KEYCLOAK_POSTGRES_USER:-edfi} + POSTGRES_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + ports: + - "5403:5432" + container_name: ed-fi-db-idp-keycloak + +volumes: + vol-db-admin-adminapi: + driver: local + name: vol-db-admin-adminapi + vol-db-idp-keycloak: + driver: local + name: vol-db-idp-keycloak diff --git a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-dev.yml b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-dev.yml new file mode 100644 index 000000000..6da92de72 --- /dev/null +++ b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-dev.yml @@ -0,0 +1,147 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + nginx: + build: + context: ../../../../Settings/V2/gateway/ + dockerfile: IDP.Dockerfile + environment: + ADMIN_API_VIRTUAL_NAME: "${ADMIN_API_VIRTUAL_NAME:-adminapi}" + ODS_VIRTUAL_NAME: "${ODS_VIRTUAL_NAME:-api}" + KEYCLOAK_VIRTUAL_NAME: ${KEYCLOAK_VIRTUAL_NAME:-auth} + ports: + - "443:443" + - "80:80" + container_name: ed-fi-gateway-adminapi + restart: always + hostname: nginx + volumes: + - ../../../../Settings/ssl:/ssl/ + depends_on: + - adminapi + - idp-keycloak + + adminapi: + build: + # Important to set the context to the root, rather than setting it to the + # EdFi.Ods.AdminApi directory, so that the Dockerfile has access to other + # C# projects. + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: dev.pgsql.Dockerfile + environment: + ADMIN_POSTGRES_HOST: db-admin + ADMIN_WAIT_POSTGRES_HOSTS: "db-admin " + AdminConsoleSettings__CorsSettings__AllowedOrigins: "${ALLOWED_ORIGINS:-https://localhost}" + AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" + AppSettings__AdminApiMode: ${ADMINAPI_MODE:-V2} + AppSettings__DatabaseEngine: "PostgreSql" + AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} + AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} + AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} + AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" + AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" + AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + AppSettings__PreventDuplicateApplications: "${PREVENT_DUPLICATE_APPLICATIONS:-false}" + Authentication__AllowRegistration: true + Authentication__IssuerUrl: ${ISSUER_URL} + Authentication__SigningKey: ${SIGNING_KEY} + ConnectionStrings__EdFi_Admin: "host=db-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + ConnectionStrings__EdFi_Security: "host=db-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + EnableDockerEnvironment: true + IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} + IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} + IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} + IpRateLimiting__ClientIdHeader: ${IPRATELIMITING__CLIENTIDHEADER:-X-ClientId} + IpRateLimiting__HttpStatusCode: ${IPRATELIMITING__HTTPSTATUSCODE:-429} + IpRateLimiting__IpWhitelist: ${IPRATELIMITING__IPWHITELIST:-[]} + IpRateLimiting__EndpointWhitelist: ${IPRATELIMITING__ENDPOINTWHITELIST:-[]} + Log4NetCore__Log4NetConfigFileName: "./log4net.config" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_PORT: "${POSTGRES_PORT:-5432}" + POSTGRES_USER: "${POSTGRES_USER}" + entrypoint: ["/bin/sh"] + command: ["-c","/app/run.sh"] + depends_on: + - db-admin + - idp-keycloak + restart: always + hostname: ${ADMIN_API_VIRTUAL_NAME:-adminapi} + container_name: adminapi + healthcheck: + test: ${ADMIN_API_HEALTHCHECK_TEST} + start_period: "60s" + retries: 3 + + db-admin: + build: + context: ../../../../ + additional_contexts: + assets: ../../../../../ + dockerfile: V2/db.pgsql.admin.Dockerfile + args: + ADMIN_API_VERSION: "${ADMIN_API_VERSION:-2.2.0}" + environment: + ADMIN_API_VERSION: ${ADMIN_API_VERSION} + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + ports: + - "5880:5432" + volumes: + - vol-db-admin-adminapi:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-admin-adminapi + + idp-keycloak: + image: quay.io/keycloak/keycloak:${KEYCLOAK_IMAGE_TAG:-26.0} + command: start-dev --import-realm + environment: + KC_HOSTNAME: "https://${KEYCLOAK_HOSTNAME:-localhost}/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_HTTP_ENABLED: true + KC_HTTPS_ENABLED: true + KC_HOSTNAME_STRICT: true + KC_HEALTH_ENABLED: ${KEYCLOAK_HEALTH_ENABLED:-true} + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://db-idp-keycloak/${KEYCLOAK_POSTGRES_DB:-keycloak_db} + KC_PROXY_HEADERS: forwarded + PROXY_ADDRESS_FORWARDING: true + KC_DB_USERNAME: ${KEYCLOAK_POSTGRES_USER:-edfi} + KC_DB_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + KC_HTTP_RELATIVE_PATH: "/${KEYCLOAK_RELATIVE_PATH:-auth}/" + KC_SPI_LOGIN_PROTOCOL_OPENID_CONNECT_LEGACY_IFRAME_CHECK: false + KC_LOG_LEVEL: ${KEYCLOAK_LOG_LEVEL:-OFF} + KC_LOG: console + KC_HTTPS_CERTIFICATE_FILE: /ssl/server.crt + KC_HTTPS_CERTIFICATE_KEY_FILE: /ssl/server.key + ports: + - ${KEYCLOAK_PORT:-28080}:8080 + restart: always + depends_on: + - db-idp-keycloak + hostname: ${KEYCLOAK_VIRTUAL_NAME:-idp_keycloak} + volumes: + - ../../../../Settings/ssl:/ssl/ + - ../../../../KeyCloak/realm-and-users.json:/opt/keycloak/data/import/realm-and-users.json + container_name: ed-fi-idp-keycloak + + db-idp-keycloak: + image: postgres:${KEYCLOAK_DB_IMAGE_TAG:-16.2} + volumes: + - vol-db-idp-keycloak:/var/lib/postgresql/data + environment: + POSTGRES_DB: ${KEYCLOAK_POSTGRES_DB:-keycloak_db} + POSTGRES_USER: ${KEYCLOAK_POSTGRES_USER:-edfi} + POSTGRES_PASSWORD: ${KEYCLOAK_POSTGRES_PASSWORD:-P@55w0rd} + container_name: ed-fi-db-idp-keycloak + +volumes: + vol-db-admin-adminapi: + driver: local + name: vol-db-admin-adminapi + vol-db-idp-keycloak: + driver: local + name: vol-db-idp-keycloak diff --git a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-ods.yml b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-ods.yml new file mode 100644 index 000000000..dadf858a1 --- /dev/null +++ b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-ods.yml @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +services: + db-ods: + image: edfialliance/ods-api-db-ods-minimal:${TAG} + environment: + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + TPDM_ENABLED: "${TPDM_ENABLED:-true}" + volumes: + - vol-db-ods:/var/lib/postgresql/data + restart: always + container_name: ed-fi-db-ods + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + start_period: "60s" + retries: 3 + + api: + image: edfialliance/ods-api-web-api:${TAG} + environment: + ADMIN_POSTGRES_HOST: db-admin + API_HEALTHCHECK_TEST: ${API_HEALTHCHECK_TEST?Please consult env.example to set the API healthcheck test} + ConnectionStrings__EdFi_Admin: "host=db-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" + ConnectionStrings__EdFi_Security: "host=db-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" + NPG_API_MAX_POOL_SIZE_ADMIN: "${NPG_API_MAX_POOL_SIZE_ADMIN}" + NPG_API_MAX_POOL_SIZE_MASTER: "${NPG_API_MAX_POOL_SIZE_MASTER}" + NPG_API_MAX_POOL_SIZE_ODS: "${NPG_API_MAX_POOL_SIZE_ODS}" + NPG_API_MAX_POOL_SIZE_SECURITY: "${NPG_API_MAX_POOL_SIZE_SECURITY}" + NPG_POOLING_ENABLED: "${NPG_POOLING_ENABLED:-false}" + ODS_CONNECTION_STRING_ENCRYPTION_KEY: "${ODS_CONNECTION_STRING_ENCRYPTION_KEY}" + ODS_WAIT_POSTGRES_HOSTS: "db-ods " + ODS_POSTGRES_HOST: db-ods + PATH_BASE: "${ODS_VIRTUAL_NAME:-api}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_PORT: "${POSTGRES_PORT:-5432}" + POSTGRES_USER: "${POSTGRES_USER}" + TPDM_ENABLED: "${TPDM_ENABLED:-true}" + volumes: + - ${LOGS_FOLDER}:/app/logs + depends_on: + - db-ods + - db-admin + restart: always + hostname: api + container_name: ed-fi-ods-api + healthcheck: + test: $$API_HEALTHCHECK_TEST + start_period: "60s" + retries: 3 + +volumes: + vol-db-admin: + driver: local + name: vol-db-admin + vol-db-ods: + driver: local + name: vol-db-ods diff --git a/Docker/V2/Compose/pgsql/env-idp.example b/Docker/V2/Compose/pgsql/env-idp.example new file mode 100644 index 000000000..d808832a1 --- /dev/null +++ b/Docker/V2/Compose/pgsql/env-idp.example @@ -0,0 +1,73 @@ +ADMIN_API_VIRTUAL_NAME=adminapi +ADMIN_API_VERSION= 2.2.2-alpha.0.78 +IPRATELIMITING__ENABLEENDPOINTRATELIMITING=false +IPRATELIMITING__STACKBLOCKEDREQUESTS=false +IPRATELIMITING__REALIPHEADER=X-Real-IP +IPRATELIMITING__CLIENTIDHEADER=X-ClientId +IPRATELIMITING__HTTPSTATUSCODE=429 +IPRATELIMITING__IPWHITELIST=[] +IPRATELIMITING__ENDPOINTWHITELIST=[] + +# For Authentication +AUTHORITY=http://localhost/${ADMIN_API_VIRTUAL_NAME} +ISSUER_URL=https://localhost/${ADMIN_API_VIRTUAL_NAME} +SIGNING_KEY= + +# For Ed-Fi ODS / API +EDFI_API_DISCOVERY_URL=https://localhost/api/ +IGNORES_CERTIFICATE_ERRORS=true +ENABLE_APPLICATION_RESET_ENDPOINT=true + +PAGING_OFFSET=0 +PAGING_LIMIT=25 + +# For Postgres only +POSTGRES_USER=postgres +POSTGRES_PASSWORD=P@ssw0rd +POSTGRES_PORT=5432 + + +# For MSSQL only +MSSQL_USER=postgres +MSSQL_PASSWORD=P@ssw0rd +MSSQL_PORT=14333 + +# The following needs to be set to specify a health check test for Admin api. +# RECOMMENDED: To use the default internal Admin Api health check endpoint, set the variable as follows: +ADMIN_API_HEALTHCHECK_TEST="wget -nv -t1 --spider http://${ADMIN_API_VIRTUAL_NAME}/health || exit 1" + +# To disable the health check, remove the above and instead set the variable as follows: +# ADMIN_API_HEALTHCHECK_TEST=/bin/true +# To add a custom health check, consult the documentation at https://docs.docker.com/compose/compose-file/compose-file-v3/#healthcheck + +# IdP db keycloak +KEYCLOAK_DB_IMAGE_TAG=16.2 +KEYCLOAK_POSTGRES_DB=keycloak_db +KEYCLOAK_POSTGRES_USER=edfi +KEYCLOAK_POSTGRES_PASSWORD=P@55w0rd +# IdP keycloak +KEYCLOAK_IMAGE_TAG=26.0 +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_PORT=28080 +KEYCLOAK_VIRTUAL_NAME=keycloak +KEYCLOAK_HOSTNAME= localhost +KEYCLOAK_HOSTNAME_PORT=443 +KEYCLOAK_HOSTNAME_STRICT_BACKCHANNEL=false +KEYCLOAK_HTTP_ENABLED=true +KEYCLOAK_HOSTNAME_STRICT_HTTPS=true +KEYCLOAK_HEALTH_ENABLED=true +KEYCLOAK_ADMIN_CONSOLE_REALM=edfi-admin-console + + +ENABLE_ADMIN_CONSOLE=true +USE_SELF_CONTAINED_AUTH=false +# OIDC_AUTHORITY=http://ed-fi-idp-keycloak:8080/auth/realms/edfi-admin-console +OIDC_REQUIRE_METADATA=false +OIDC_ENABLE_SERVER_CERTIFICATE=true + + +# Ods Api +TAG=7.3 +API_HEALTHCHECK_TEST="curl -f http://localhost/health" +ODS_CONNECTION_STRING_ENCRYPTION_KEY= diff --git a/Docker/V2/Compose/pgsql/env.example b/Docker/V2/Compose/pgsql/env.example new file mode 100644 index 000000000..5bbcffde7 --- /dev/null +++ b/Docker/V2/Compose/pgsql/env.example @@ -0,0 +1,47 @@ +ADMIN_API_VIRTUAL_NAME=adminapi +ADMIN_API_VERSION= 2.2.2-alpha.0.78 +IPRATELIMITING__ENABLEENDPOINTRATELIMITING=false +IPRATELIMITING__STACKBLOCKEDREQUESTS=false +IPRATELIMITING__REALIPHEADER=X-Real-IP +IPRATELIMITING__CLIENTIDHEADER=X-ClientId +IPRATELIMITING__HTTPSTATUSCODE=429 +IPRATELIMITING__IPWHITELIST=[] +IPRATELIMITING__ENDPOINTWHITELIST=[] + +# For Authentication +AUTHORITY=http://localhost/${ADMIN_API_VIRTUAL_NAME} +ISSUER_URL=https://localhost/${ADMIN_API_VIRTUAL_NAME} +SIGNING_KEY= + +# For Ed-Fi ODS / API +EDFI_API_DISCOVERY_URL=https://localhost/api/ +IGNORES_CERTIFICATE_ERRORS=true +ENABLE_APPLICATION_RESET_ENDPOINT=true + +PAGING_OFFSET=0 +PAGING_LIMIT=25 + +# For Postgres only +POSTGRES_USER=postgres +POSTGRES_PASSWORD=P@ssw0rd +POSTGRES_PORT=5432 + + +# For MSSQL only +MSSQL_USER=postgres +MSSQL_PASSWORD=P@ssw0rd +MSSQL_PORT=14333 + +# The following needs to be set to specify a health check test for Admin api. +# RECOMMENDED: To use the default internal Admin Api health check endpoint, set the variable as follows: +ADMIN_API_HEALTHCHECK_TEST="wget -nv -t1 --spider http://${ADMIN_API_VIRTUAL_NAME}/health || exit 1" + +# To disable the health check, remove the above and instead set the variable as follows: +# ADMIN_API_HEALTHCHECK_TEST=/bin/true +# To add a custom health check, consult the documentation at https://docs.docker.com/compose/compose-file/compose-file-v3/#healthcheck + + +# Ods Api +TAG=7.3 +API_HEALTHCHECK_TEST="curl -f http://localhost/health" +ODS_CONNECTION_STRING_ENCRYPTION_KEY= diff --git a/Docker/V2/db.mssql.admin.Dockerfile b/Docker/V2/db.mssql.admin.Dockerfile new file mode 100644 index 000000000..e1da72384 --- /dev/null +++ b/Docker/V2/db.mssql.admin.Dockerfile @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +FROM mcr.microsoft.com/mssql/server@sha256:d7f2c670f0cd807b4dc466b8887bd2b39a4561f624c154896f5564ea38efd13a AS base +USER root +RUN apt-get update && apt-get install unzip -y dos2unix busybox openssl libxml2 +LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " + +ENV MSSQL_USER=${SQLSERVER_USER} +ENV MSSQL_PASSWORD=${SQLSERVER_PASSWORD} +ENV MSSQL_DB=master + +ARG STANDARD_VERSION="5.0.0" +ARG ADMIN_VERSION="7.2.49" +ARG SECURITY_VERSION="7.2.48" + +USER root + +RUN useradd -M -s /bin/bash -u 10099 -g 0 edfi && \ + mkdir -p -m 770 /var/opt/edfi && chgrp -R 0 /var/opt/edfi && \ + wget -nv -O /tmp/sqlpackage.zip "https://aka.ms/sqlpackage-linux" && \ + unzip -o /tmp/sqlpackage.zip -d /opt/sqlpackage && \ + chmod +x /opt/sqlpackage/sqlpackage +FROM base AS setup + +USER root + +COPY Settings/V2/DB-Admin/mssql/healthcheck.sh /usr/local/bin/healthcheck.sh +RUN chmod +x /usr/local/bin/healthcheck.sh + +COPY Settings/V2/DB-Admin/mssql/init-database.sh /tmp/init/3-init-database.sh +COPY Settings/V2/DB-Admin/mssql/entrypoint.sh /tmp/init/entrypoint.sh + +COPY Settings/V2/DB-Admin/mssql/run-adminapi-migrations.sh /docker-entrypoint-initdb.d/3-run-adminapi-migrations.sh +COPY --from=assets Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Admin/ /tmp/AdminApiScripts/Admin/MsSql +COPY --from=assets Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Security/ /tmp/AdminApiScripts/Security/MsSql +COPY Settings/dev/adminapi-test-seeddata.sql /tmp/AdminApiScripts/Admin/MsSql/adminapi-test-seeddata.sql + +RUN wget -q -O /tmp/EdFi_Admin.zip "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_apis/packaging/feeds/EdFi/nuget/packages/EdFi.Database.Admin.BACPAC.Standard.${STANDARD_VERSION}/versions/${ADMIN_VERSION}/content" && \ + wget -q -O /tmp/EdFi_Security.zip "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_apis/packaging/feeds/EdFi/nuget/packages/EdFi.Database.Security.BACPAC.Standard.${STANDARD_VERSION}/versions/${SECURITY_VERSION}/content" && \ + unzip -p /tmp/EdFi_Admin.zip EdFi_Admin.bacpac > /tmp/EdFi_Admin.bacpac && \ + unzip -p /tmp/EdFi_Security.zip EdFi_Security.bacpac > /tmp/EdFi_Security.bacpac && \ + dos2unix /tmp/EdFi_Admin.bacpac && \ + dos2unix /tmp/EdFi_Security.bacpac && \ + dos2unix /tmp/init/3-init-database.sh && \ + chmod +x /tmp/init/3-init-database.sh && \ + rm -f /tmp/EdFi_Admin.zip && \ + rm -f /tmp/EdFi_Security.zip && \ + dos2unix /docker-entrypoint-initdb.d/3-run-adminapi-migrations.sh && \ + # Admin + dos2unix /tmp/AdminApiScripts/Admin/MsSql/* && \ + chmod -R 777 /tmp/AdminApiScripts/Admin/MsSql/* && \ + # Security + dos2unix /tmp/AdminApiScripts/Security/MsSql/* && \ + chmod -R 777 /tmp/AdminApiScripts/Security/MsSql/* + +EXPOSE 1433 + +USER edfi + +CMD ["/bin/bash", "/tmp/init/entrypoint.sh"] diff --git a/Docker/V2/db.pgsql.admin.Dockerfile b/Docker/V2/db.pgsql.admin.Dockerfile new file mode 100644 index 000000000..a4334e31f --- /dev/null +++ b/Docker/V2/db.pgsql.admin.Dockerfile @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: Apache-2.0 +# Licensed to the Ed-Fi Alliance under one or more agreements. +# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +# See the LICENSE and NOTICES files in the project root for more information. + +FROM alpine:3.20@sha256:187cce89a2fdd4eaf457a0af45f5ce27672f35ce0f6df49b5b0ee835afe0561b AS assets + +FROM edfialliance/ods-api-db-admin:7.3.1@sha256:9d6c6ad298f5eb2ea58d7b2c1c7ea5f6bdfcd12d90028b98fbfea4237a5610f2 AS base +USER root +RUN apk add --no-cache dos2unix=7.5.2-r0 unzip=6.0-r15 && rm -rf /var/cache/apk/* + +FROM base AS setup +LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " + +USER root + +COPY --from=assets Docker/Settings/V2/DB-Admin/pgsql/run-adminapi-migrations.sh /docker-entrypoint-initdb.d/3-run-adminapi-migrations.sh +COPY --from=assets Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Admin/ /tmp/AdminApiScripts/Admin/PgSql +COPY --from=assets Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Security/ /tmp/AdminApiScripts/Security/PgSql +COPY --from=assets Docker/Settings/dev/adminapi-test-seeddata.sql /tmp/AdminApiScripts/Admin/PgSql/adminapi-test-seeddata.sql + +RUN dos2unix /docker-entrypoint-initdb.d/3-run-adminapi-migrations.sh && \ + #Admin + dos2unix /tmp/AdminApiScripts/Admin/PgSql/* && \ + chmod -R 777 /tmp/AdminApiScripts/Admin/PgSql/* && \ + #Security + dos2unix /tmp/AdminApiScripts/Security/PgSql/* && \ + chmod -R 777 /tmp/AdminApiScripts/Security/PgSql/* && \ + # Clean up + apk del unzip dos2unix + +USER postgres + +EXPOSE 5432 + +CMD ["docker-entrypoint.sh", "postgres"] diff --git a/Docker/api.mssql.Dockerfile b/Docker/api.mssql.Dockerfile index 45eef0802..af3be48b1 100644 --- a/Docker/api.mssql.Dockerfile +++ b/Docker/api.mssql.Dockerfile @@ -3,32 +3,32 @@ # The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. # See the LICENSE and NOTICES files in the project root for more information. -FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.19-amd64@sha256:edc046db633d2eac3acfa494c10c6b7b3b9ff9f66f1ed92cec8021f5ee38d755 as base -RUN apk --no-cache add curl=~8 unzip=~6 dos2unix=~7 bash=~5 gettext=~0 jq=~1 icu=~74 && \ - +FROM mcr.microsoft.com/dotnet/aspnet:8.0.21-alpine3.21-amd64@sha256:61adf767314cc4b6a298dd3bdba46a2f10be37d67c75ad64dc7a89a44df8a228 AS base +RUN apk upgrade --no-cache && \ + apk add --no-cache unzip=~6 dos2unix=~7 bash=~5 gettext=~0 jq=~1 icu=~74 openssl=3.3.5-r0 musl=~1.2.5-r9 && \ addgroup -S edfi && adduser -S edfi -G edfi -FROM base as build - +FROM base AS build LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " -ARG DB="mssql" -ARG VERSION="1.4.3" +# Alpine image does not contain Globalization Cultures library so we need to install ICU library to get for LINQ expression to work +# Disable the globaliztion invariant mode (set in base image) ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +ARG ADMIN_API_VERSION +ENV ADMIN_API_VERSION="${ADMIN_API_VERSION:-2.2.0}" ENV ASPNETCORE_HTTP_PORTS=80 WORKDIR /app -COPY --chmod=600 Settings/"${DB}"/appsettings.template.json /app/appsettings.template.json -COPY --chmod=500 Settings/"${DB}"/run.sh /app/run.sh -COPY Settings/"${DB}"/log4net.config /app/log4net.txt +COPY --chmod=500 Settings/mssql/run.sh /app/run.sh +COPY Settings/mssql/log4net.config /app/log4net.txt -RUN umask 0077 && \ - wget -nv -O /tmp/msodbcsql18_18.4.1.1-1_amd64.apk https://download.microsoft.com/download/7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8/msodbcsql18_18.4.1.1-1_amd64.apk && \ +RUN umask 0077 && \ + wget -nv -O /tmp/msodbcsql18_18.4.1.1-1_amd64.apk https://download.microsoft.com/download/7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8/msodbcsql18_18.4.1.1-1_amd64.apk && \ wget -nv -O /tmp/mssql-tools18_18.4.1.1-1_amd64.apk https://download.microsoft.com/download/7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8/mssql-tools18_18.4.1.1-1_amd64.apk && \ apk --no-cache add --allow-untrusted /tmp/msodbcsql18_18.4.1.1-1_amd64.apk && \ apk --no-cache add --allow-untrusted /tmp/mssql-tools18_18.4.1.1-1_amd64.apk && \ - wget -nv -O /app/AdminApi.zip "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_apis/packaging/feeds/EdFi/nuget/packages/EdFi.Suite3.ODS.AdminApi/versions/${VERSION}/content" && \ + wget -nv -O /app/AdminApi.zip "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_apis/packaging/feeds/EdFi/nuget/packages/EdFi.Suite3.ODS.AdminApi/versions/${ADMIN_API_VERSION}/content" && \ unzip /app/AdminApi.zip AdminApi/* -d /app/ && \ cp -r /app/AdminApi/. /app/ && \ rm -f /app/AdminApi.zip && \ @@ -37,12 +37,13 @@ RUN umask 0077 && \ dos2unix /app/*.json && \ dos2unix /app/*.sh && \ dos2unix /app/log4net.config && \ - chmod 700 /app/*.sh -- ** && \ + chmod 700 /app/*.sh -- ** && \ rm -f /app/*.exe && \ - apk del unzip dos2unix curl && \ + apk del unzip dos2unix && \ chown -R edfi /app EXPOSE ${ASPNETCORE_HTTP_PORTS} USER edfi ENTRYPOINT [ "/app/run.sh" ] + diff --git a/Docker/api.pgsql.Dockerfile b/Docker/api.pgsql.Dockerfile index ca3cbc440..f8c5a948a 100644 --- a/Docker/api.pgsql.Dockerfile +++ b/Docker/api.pgsql.Dockerfile @@ -3,29 +3,30 @@ # The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. # See the LICENSE and NOTICES files in the project root for more information. -FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.19-amd64@sha256:edc046db633d2eac3acfa494c10c6b7b3b9ff9f66f1ed92cec8021f5ee38d755 as base -ARG DB=pgsql - -RUN apk --no-cache add curl=~8 unzip=~6 dos2unix=~7 bash=~5 gettext=~0 icu=~74 jq=~1 && \ - if [ "$DB" = "pgsql" ]; then apk --no-cache add postgresql13-client=~13; fi && \ +#tag 8.0-alpine +FROM mcr.microsoft.com/dotnet/aspnet:8.0.21-alpine3.22@sha256:cb69be896f82e0d73f513c128ece501c7c1f1809c49415a69dc096e013d5314a AS base +RUN apk upgrade --no-cache && \ + apk add --no-cache bash=~5 dos2unix=~7 gettext=~0 icu=~76.1-r1 jq=~1 musl=~1.2.5-r10 openssl=3.5.4-r0 postgresql15-client=~15 unzip=~6 && \ + rm -rf /var/cache/apk/* && \ addgroup -S edfi && adduser -S edfi -G edfi -FROM base as build - +FROM base AS build LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " -ARG VERSION=latest +# Alpine image does not contain Globalization Cultures library so we need to install ICU library to get for LINQ expression to work +# Disable the globaliztion invariant mode (set in base image) ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +ARG ADMIN_API_VERSION +ENV ADMIN_API_VERSION="${ADMIN_API_VERSION:-2.2.0}" ENV ASPNETCORE_HTTP_PORTS=80 WORKDIR /app -COPY --chmod=600 Settings/"${DB}"/appsettings.template.json /app/appsettings.template.json -COPY --chmod=500 Settings/"${DB}"/run.sh /app/run.sh -COPY Settings/"${DB}"/log4net.config /app/log4net.txt +COPY --chmod=500 Settings/pgsql/run.sh /app/run.sh +COPY Settings/pgsql/log4net.config /app/log4net.txt RUN umask 0077 && \ - wget -nv -O /app/AdminApi.zip "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_apis/packaging/feeds/EdFi/nuget/packages/EdFi.Suite3.ODS.AdminApi/versions/${VERSION}/content" && \ + wget -nv -O /app/AdminApi.zip "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_apis/packaging/feeds/EdFi/nuget/packages/EdFi.Suite3.ODS.AdminApi/versions/${ADMIN_API_VERSION}/content" && \ unzip /app/AdminApi.zip AdminApi/* -d /app/ && \ cp -r /app/AdminApi/. /app/ && \ rm -f /app/AdminApi.zip && \ @@ -34,8 +35,9 @@ RUN umask 0077 && \ dos2unix /app/*.json && \ dos2unix /app/*.sh && \ dos2unix /app/log4net.config && \ + chmod 700 /app/*.sh -- ** && \ rm -f /app/*.exe && \ - apk del unzip dos2unix curl && \ + apk del unzip dos2unix && \ chown -R edfi /app EXPOSE ${ASPNETCORE_HTTP_PORTS} diff --git a/Docker/dev.mssql.Dockerfile b/Docker/dev.mssql.Dockerfile index 5a9e82fca..f4c8c510e 100644 --- a/Docker/dev.mssql.Dockerfile +++ b/Docker/dev.mssql.Dockerfile @@ -3,55 +3,62 @@ # The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. # See the LICENSE and NOTICES files in the project root for more information. -# First layer uses a dotnet/sdk image to build the Admin API from source code -# Second layer uses the dotnet/aspnet image to run the built code +# First two layers use a dotnet/sdk image to build the Admin API from source +# code. The next two layers use the dotnet/aspnet image to run the built code. +# The extra layers in the middle support caching of base layers. -FROM mcr.microsoft.com/dotnet/sdk:8.0.203-alpine3.19@sha256:b1275049a8fe922cbc9f1d173ffec044664f30b94e99e2c85dd9b7454fbf596c AS buildbase -RUN apk --no-cache add curl=~8 -FROM buildbase AS publish -WORKDIR /source +FROM mcr.microsoft.com/dotnet/sdk:8.0.415-alpine3.21@sha256:f308a8fe0941a318421d18a0917b344d15d18996173a2db6f908a12b8db6b074 AS build +RUN apk upgrade --no-cache && apk add --no-cache musl=~1.2.5-r9 +ARG ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-"Production"} +WORKDIR /source +COPY --from=assets ./Application/NuGet.Config ./ +COPY --from=assets ./Application/Directory.Packages.props ./ COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi/ COPY --from=assets ./Application/EdFi.Ods.AdminApi EdFi.Ods.AdminApi/ +RUN rm -f EdFi.Ods.AdminApi/appsettings.Development.json + +COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.Common/ +COPY --from=assets ./Application/EdFi.Ods.AdminApi.Common EdFi.Ods.AdminApi.Common/ +COPY --from=assets ./Application/EdFi.Ods.AdminApi.V1 EdFi.Ods.AdminApi.V1/ WORKDIR /source/EdFi.Ods.AdminApi +RUN export ASPNETCORE_ENVIRONMENT=$ASPNETCORE_ENVIRONMENT RUN dotnet restore && dotnet build -c Release +RUN dotnet publish -c Release /p:EnvironmentName=$ASPNETCORE_ENVIRONMENT --no-build -o /app/EdFi.Ods.AdminApi -RUN dotnet publish -c Release /p:EnvironmentName=Production --no-build -o /app/EdFi.Ods.AdminApi - -FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.19-amd64@sha256:edc046db633d2eac3acfa494c10c6b7b3b9ff9f66f1ed92cec8021f5ee38d755 AS runtimebase -FROM runtimebase AS runtime -RUN apk --no-cache add curl=~8 dos2unix=~7 bash=~5 gettext=~0 icu=~74 && \ +FROM mcr.microsoft.com/dotnet/aspnet:8.0.21-alpine3.21-amd64@sha256:61adf767314cc4b6a298dd3bdba46a2f10be37d67c75ad64dc7a89a44df8a228 AS runtimebase +RUN apk upgrade --no-cache && \ + apk add dos2unix=~7 bash=~5 gettext=~0 icu=~74 curl musl=~1.2.5-r9 && \ addgroup -S edfi && adduser -S edfi -G edfi -FROM runtime AS setup +FROM runtimebase AS setup LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " +# Alpine image does not contain Globalization Cultures library so we need to install ICU library to get for LINQ expression to work +# Disable the globaliztion invariant mode (set in base image) ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false -ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_HTTP_PORTS=80 - -COPY --chmod=500 Settings/dev/mssql/run.sh /app/run.sh -COPY Settings/dev/log4net.config /app/log4net.txt +ENV DB_FOLDER=mssql WORKDIR /app -COPY --from=publish /app/EdFi.Ods.AdminApi . +COPY --from=build /app/EdFi.Ods.AdminApi . + +COPY --chmod=500 --from=assets Docker/Settings/dev/${DB_FOLDER}/run.sh /app/run.sh +COPY --from=assets Docker/Settings/dev/log4net.config /app/log4net.txt RUN cp /app/log4net.txt /app/log4net.config && \ dos2unix /app/*.json && \ dos2unix /app/*.sh && \ dos2unix /app/log4net.config && \ chmod 500 /app/*.sh -- ** && \ - rm -f /app/log4net.txt && \ - rm -f /app/*.exe && \ - apk del dos2unix && \ - mkdir -p /app && chmod -R 777 /app && \ - chown -R edfi /app && \ + chown -R edfi /app && \ wget -nv -O /tmp/msodbcsql18_18.4.1.1-1_amd64.apk https://download.microsoft.com/download/7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8/msodbcsql18_18.4.1.1-1_amd64.apk && \ wget -nv -O /tmp/mssql-tools18_18.4.1.1-1_amd64.apk https://download.microsoft.com/download/7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8/mssql-tools18_18.4.1.1-1_amd64.apk && \ apk --no-cache add --allow-untrusted /tmp/msodbcsql18_18.4.1.1-1_amd64.apk && \ - apk --no-cache add --allow-untrusted /tmp/mssql-tools18_18.4.1.1-1_amd64.apk + apk --no-cache add --allow-untrusted /tmp/mssql-tools18_18.4.1.1-1_amd64.apk && \ + apk del dos2unix EXPOSE ${ASPNETCORE_HTTP_PORTS} USER edfi diff --git a/Docker/dev.pgsql.Dockerfile b/Docker/dev.pgsql.Dockerfile index f3cc67d4e..30e51ef83 100644 --- a/Docker/dev.pgsql.Dockerfile +++ b/Docker/dev.pgsql.Dockerfile @@ -3,49 +3,71 @@ # The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. # See the LICENSE and NOTICES files in the project root for more information. -# First layer uses a dotnet/sdk image to build the Admin API from source code -# Second layer uses the dotnet/aspnet image to run the built code +# First two layers use a dotnet/sdk image to build the Admin API from source +# code. The next two layers use the dotnet/aspnet image to run the built code. +# The extra layers in the middle support caching of base layers. +# Define assets stage using Alpine 3.21 to match the version used in other stages +FROM alpine:3.21@sha256:5405e8f36ce1878720f71217d664aa3dea32e5e5df11acbf07fc78ef5661465b AS assets + +FROM mcr.microsoft.com/dotnet/sdk:8.0.415-alpine3.21@sha256:f308a8fe0941a318421d18a0917b344d15d18996173a2db6f908a12b8db6b074 AS build +RUN apk add --no-cache musl=1.2.5-r9 && \ + rm -rf /var/cache/apk/* + +ARG ASPNETCORE_ENVIRONMENT="Production" +ENV ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT} -FROM mcr.microsoft.com/dotnet/sdk:8.0.203-alpine3.19@sha256:b1275049a8fe922cbc9f1d173ffec044664f30b94e99e2c85dd9b7454fbf596c AS build WORKDIR /source +COPY --from=assets ./Application/NuGet.Config ./ +COPY --from=assets ./Application/Directory.Packages.props ./ +COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi/ +COPY --from=assets ./Application/EdFi.Ods.AdminApi EdFi.Ods.AdminApi/ +RUN rm -f EdFi.Ods.AdminApi/appsettings.Development.json -COPY --from=assets Application/NuGet.Config EdFi.Ods.AdminApi/ -COPY --from=assets Application/EdFi.Ods.AdminApi EdFi.Ods.AdminApi/ +COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.Common/ +COPY --from=assets ./Application/EdFi.Ods.AdminApi.Common EdFi.Ods.AdminApi.Common/ + +COPY --from=assets ./Application/EdFi.Ods.AdminApi.V1 EdFi.Ods.AdminApi.V1/ WORKDIR /source/EdFi.Ods.AdminApi +RUN export ASPNETCORE_ENVIRONMENT=$ASPNETCORE_ENVIRONMENT RUN dotnet restore && dotnet build -c Release +RUN dotnet publish -c Release /p:EnvironmentName=$ASPNETCORE_ENVIRONMENT --no-build -o /app/EdFi.Ods.AdminApi -FROM build AS publish -RUN dotnet publish -c Release /p:EnvironmentName=Production --no-build -o /app/EdFi.Ods.AdminApi - -FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.19-amd64@sha256:edc046db633d2eac3acfa494c10c6b7b3b9ff9f66f1ed92cec8021f5ee38d755 as base - -RUN apk upgrade --no-cache && apk add curl=~8 dos2unix=~7 bash=~5 gettext=~0 icu=~74 'musl>=1.2.4_git20230717-r5' && \ +FROM mcr.microsoft.com/dotnet/aspnet:8.0.21-alpine3.21-amd64@sha256:61adf767314cc4b6a298dd3bdba46a2f10be37d67c75ad64dc7a89a44df8a228 AS runtimebase +RUN apk add --no-cache \ + bash=5.2.37-r0 \ + dos2unix=7.5.2-r0 \ + gettext=0.22.5-r0 \ + icu=74.2-r1 \ + musl=1.2.5-r9 \ + openssl=3.3.5-r0 \ + postgresql15-client=15.13-r0 && \ + rm -rf /var/cache/apk/* && \ addgroup -S edfi && adduser -S edfi -G edfi -FROM base AS setup +FROM runtimebase AS setup LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " +# Alpine image does not contain Globalization Cultures library so we need to install ICU library to get for LINQ expression to work +# Disable the globaliztion invariant mode (set in base image) ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false -ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_HTTP_PORTS=80 - -COPY --chmod=500 Settings/dev/pgsql/run.sh /app/run.sh -COPY Settings/dev/log4net.config /app/log4net.txt +ENV DB_FOLDER=pgsql WORKDIR /app -COPY --from=publish /app/EdFi.Ods.AdminApi . +COPY --from=build /app/EdFi.Ods.AdminApi . + +COPY --chmod=500 --from=assets Docker/Settings/dev/${DB_FOLDER}/run.sh /app/run.sh +COPY --from=assets Docker/Settings/dev/log4net.config /app/log4net.txt RUN cp /app/log4net.txt /app/log4net.config && \ dos2unix /app/*.json && \ dos2unix /app/*.sh && \ dos2unix /app/log4net.config && \ chmod 500 /app/*.sh -- ** && \ - rm -f /app/log4net.txt && \ - rm -f /app/*.exe && \ - apk del dos2unix && \ - chown -R edfi /app + chown -R edfi /app && \ + apk del dos2unix EXPOSE ${ASPNETCORE_HTTP_PORTS} USER edfi diff --git a/Installer.AdminApi/Install-AdminApi.psm1 b/Installer.AdminApi/Install-AdminApi.psm1 index 2be1a3346..191a52290 100644 --- a/Installer.AdminApi/Install-AdminApi.psm1 +++ b/Installer.AdminApi/Install-AdminApi.psm1 @@ -49,8 +49,6 @@ function Install-EdFiOdsAdminApi { PS c:\> $parameters = @{ ToolsPath = "C:/temp/tools" DbConnectionInfo = $dbConnectionInfo - OdsDatabaseName = "EdFi_Ods_Sandbox" - OdsApiVersion = "6.1" } PS c:\> Install-EdFiOdsAdminApi @parameters @@ -60,19 +58,11 @@ function Install-EdFiOdsAdminApi { .EXAMPLE PS c:\> $parameters = @{ ToolsPath = "C:/temp/tools" - OdsApiVersion = "6.1" AdminDbConnectionInfo = @{ Engine="SqlServer" Server="edfi-auth.my-sql-server.example" UseIntegratedSecurity=$true } - OdsDbConnectionInfo = @{ - DatabaseName="EdFi_ODS_Staging" - Engine="SqlServer" - Server="edfi-stage.my-sql-server.example" - Username="ods-write" - Password="@#$%^&*(GHJ%^&*YUKSDF" - } SecurityDbConnectionInfo = @{ Engine="SqlServer" Server="edfi-auth.my-sql-server.example" @@ -96,7 +86,6 @@ function Install-EdFiOdsAdminApi { ToolsPath = "C:/temp/tools" DbConnectionInfo = $dbConnectionInfo InstallCredentialsUseIntegratedSecurity = $true - OdsApiUrl = "http://example-web-api.com/WebApi" } PS c:\> Install-EdFiOdsAdminApi @parameters @@ -112,15 +101,10 @@ function Install-EdFiOdsAdminApi { PS c:\> $parameters = @{ ToolsPath = "C:/temp/tools" DbConnectionInfo = $dbConnectionInfo - OdsApiVersion = "6.1" - AdminApiFeatures = @{ - ApiMode="yearspecific" - } } PS c:\> Install-EdFiOdsAdminApi @parameters - Installs Admin Api to SQL Server in Year Specific mode for 2020. The installer will also - install Admin Api in ASP.NET Identity mode, rather than AD Authentication. + The installer will install Admin Api in ASP.NET Identity mode, rather than AD Authentication. #> [CmdletBinding()] param ( @@ -128,7 +112,7 @@ function Install-EdFiOdsAdminApi { [string] $PackageName = "EdFi.Suite3.ODS.AdminApi", - # NuGet package version. If not set, will retrieve the latest full release package. + # NuGet package version. This value is replaced by the build process. [string] $PackageVersion = "__ADMINAPI_VERSION__", @@ -165,12 +149,6 @@ function Install-EdFiOdsAdminApi { [string] $CertThumbprint, - # ODS / API version. Valid values are 5.3, 5.3-cqe, 6.0, 6.1." - [Parameter(Mandatory=$true)] - [ValidateSet('5.3', '5.3-cqe', '6.0', '6.1')] - [string] - $OdsApiVersion, - # Install Credentials: User [string] $InstallCredentialsUser, @@ -203,8 +181,10 @@ function Install-EdFiOdsAdminApi { # The hashtable must include: Server, Engine (SqlServer or PostgreSQL), and # either UseIntegratedSecurity or Username and Password (Password can be skipped # for PostgreSQL when using pgconf file). Optionally can include Port. + # This can be used with IsMultiTenant flag. [hashtable] [Parameter(Mandatory=$true, ParameterSetName="SharedCredentials")] + [Parameter(ParameterSetName="MultiTenant")] $DbConnectionInfo, # Database connectivity only for the admin database. @@ -217,16 +197,6 @@ function Install-EdFiOdsAdminApi { [Parameter(Mandatory=$true, ParameterSetName="SeparateCredentials")] $AdminDbConnectionInfo, - # Database connectivity only for the ODS database. - # - # The hashtable must include: Server, Engine (SqlServer or PostgreSQL), and - # either UseIntegratedSecurity or Username and Password (Password can be skipped - # for PostgreSQL when using pgconf file). Optionally can include Port and - # DatabaseName. - [hashtable] - [Parameter(Mandatory=$true, ParameterSetName="SeparateCredentials")] - $OdsDbConnectionInfo, - # Database connectivity only for the security database. # # The hashtable must include: Server, Engine (SqlServer or PostgreSQL), and @@ -242,17 +212,26 @@ function Install-EdFiOdsAdminApi { [Parameter(Mandatory=$true)] $AuthenticationSettings, - # Optional overrides for features and settings in the appsettings. - # - # The hashtable can include: ApiMode. By default, AdminApi is installed - # inShared Instance mode. - [hashtable] - $AdminApiFeatures, - # Database Config [switch] $NoDuration, + # Deploy Admin Api with MultiTenant support. + # Passing this flag, requires to pass Tenants configuration. + # When true, this flag will enable the MultiTenancy flag in Appsettings. + [switch] + [Parameter(Mandatory=$true, ParameterSetName="MultiTenant")] + $IsMultiTenant, + + # List of Tenants with information required by the Tenants section in appsettings.json + # + # Each tenant hashtable can include: + # - AdminDatabaseName and SecurityDatabaseName when used with DbConnectionInfo. + # - AdminDbConnectionInfo and SecurityDbConnectionInfo when DbConnectionInfo is not used. + [hashtable] + [Parameter(Mandatory=$true, ParameterSetName="MultiTenant")] + $Tenants, + # Set Encrypt=false for all connection strings # Not recomended for production environment. [switch] @@ -277,7 +256,6 @@ function Install-EdFiOdsAdminApi { WebSitePort = $WebsitePort CertThumbprint = $CertThumbprint WebApplicationName = $WebApplicationName - OdsApiVersion = $OdsApiVersion DatabaseInstallCredentials = @{ DatabaseUser = $InstallCredentialsUser DatabasePassword = $InstallCredentialsPassword @@ -288,21 +266,32 @@ function Install-EdFiOdsAdminApi { SecurityDatabaseName = $SecurityDatabaseName DbConnectionInfo = $DbConnectionInfo AdminDbConnectionInfo = $AdminDbConnectionInfo - OdsDbConnectionInfo = $OdsDbConnectionInfo SecurityDbConnectionInfo = $SecurityDbConnectionInfo AuthenticationSettings = $AuthenticationSettings - AdminApiFeatures = $AdminApiFeatures NoDuration = $NoDuration + IsMultiTenant = $IsMultiTenant.IsPresent + Tenants = $Tenants UnEncryptedConnection = $UnEncryptedConnection } + if($IsMultiTenant.IsPresent) + { + Write-Warning "Please make sure required tenant specific Admin, Security databases are already available on the data server." + } + $elapsed = Use-StopWatch { $result += Invoke-InstallationPreCheck -Config $Config $result += Initialize-Configuration -Config $config $result += Set-AdminApiPackageSource -Config $Config $result += Get-DbDeploy -Config $Config $result += Invoke-TransformAppSettings -Config $Config - $result += Invoke-TransformConnectionStrings -Config $config + + if ($IsMultiTenant.IsPresent) { + $result += Invoke-TransformMultiTenantConnectionStrings -Config $config + } else { + $result += Invoke-TransformConnectionStrings -Config $config + } + $result += Install-Application -Config $Config $result += Set-SqlLogins -Config $Config $result += Invoke-DbUpScripts -Config $Config @@ -331,7 +320,7 @@ function Update-EdFiOdsAdminApi { Invokes dbup migrations for updating the EdFi_Admin database accordingly. .EXAMPLE PS c:\> $parameters = @{ - PackageVersion = '1.1.0' + PackageVersion = "__ADMINAPI_VERSION__" } PS c:\> Upgrade-AdminApi @parameters @@ -343,7 +332,7 @@ function Update-EdFiOdsAdminApi { [string] $PackageName = "EdFi.Suite3.ODS.AdminApi", - # NuGet package version. If not set, will retrieve the latest full release package. + # NuGet package version. This value is replaced by the build process. [string] $PackageVersion = "__ADMINAPI_VERSION__", @@ -380,12 +369,6 @@ function Update-EdFiOdsAdminApi { [string] $CertThumbprint, - # ODS / API version. Valid values are 5.3, 5.3-cqe, 6.0, 6.1." - [Parameter(Mandatory=$true)] - [ValidateSet('5.3', '5.3-cqe', '6.0', '6.1')] - [string] - $OdsApiVersion, - # Install Credentials: User [Parameter(ParameterSetName="InstallCredentials")] [string] @@ -440,7 +423,6 @@ function Update-EdFiOdsAdminApi { NoDuration = $NoDuration ApplicationInstallType = "Upgrade" AdminDbConnectionInfo = $AdminDbConnectionInfo - OdsApiVersion = $OdsApiVersion } $elapsed = Use-StopWatch { @@ -597,15 +579,6 @@ function Invoke-InstallationPreCheck{ exit } } - - if($Config.AdminApiFeatures.ContainsKey("ApiMode") -and $Config.AdminApiFeatures.ApiMode) { - $apiMode = $Config.AdminApiFeatures.ApiMode - $supportedModes = @('sandbox', 'sharedinstance', 'yearspecific', 'districtspecific') - if ($supportedModes -NotContains $apiMode) { - Write-Warning "Not supported ApiMode: '$apiMode'. Please use one of the supported modes for the ApiMode Admin Api feature. Supported modes:'$($supportedModes -join "','")'" - exit - } - } } } @@ -773,7 +746,7 @@ function Invoke-TransferAppsettings { $backUpPath = $Config.ApplicationBackupPath Write-Warning "The following appsettings will be copied over from existing application: " - $appSettings = @('ProductionApiUrl','DatabaseEngine', 'ApiStartupType', 'ApiExternalUrl', 'PathBase', 'Log4NetConfigFileName', 'Authority', 'IssuerUrl', 'SigningKey', 'AllowRegistration') + $appSettings = @('DatabaseEngine', 'ApiStartupType', 'ApiExternalUrl', 'PathBase', 'Log4NetConfigFileName', 'Authority', 'IssuerUrl', 'SigningKey', 'AllowRegistration') foreach ($property in $appSettings) { Write-Host $property; } @@ -783,7 +756,6 @@ function Invoke-TransferAppsettings { $newSettingsFile = Join-Path $Config.WebConfigLocation "appsettings.json" $newSettings = Get-Content $newSettingsFile | ConvertFrom-Json | ConvertTo-Hashtable - $newSettings.AppSettings.ProductionApiUrl = $oldSettings.AppSettings.ProductionApiUrl $newSettings.AppSettings.DatabaseEngine = $oldSettings.AppSettings.DatabaseEngine $newSettings.AppSettings.ApiStartupType = $oldSettings.AppSettings.ApiStartupType $newSettings.AppSettings.ApiExternalUrl = $oldSettings.AppSettings.ApiExternalUrl @@ -795,8 +767,6 @@ function Invoke-TransferAppsettings { $newSettings.Authentication.SigningKey = $oldSettings.Authentication.SigningKey $newSettings.Authentication.AllowRegistration = $oldSettings.Authentication.AllowRegistration - $newSettings.AppSettings.OdsApiVersion = $Config.OdsApiVersion - $EmptyHashTable=@{} $mergedSettings = Merge-Hashtables $newSettings, $EmptyHashTable New-JsonFile $newSettingsFile $mergedSettings -Overwrite @@ -897,17 +867,23 @@ function Initialize-Configuration { $Config.usingSharedCredentials = $Config.ContainsKey("DbConnectionInfo") -and (-not $null -eq $Config.DbConnectionInfo) if ($Config.usingSharedCredentials) { Assert-DatabaseConnectionInfo -DbConnectionInfo $Config.DbConnectionInfo - $Config.DbConnectionInfo.ApplicationName = "Ed-Fi ODS/API AdminApi" + $Config.DbConnectionInfo.ApplicationName = "AdminApi" $Config.engine = $Config.DbConnectionInfo.Engine } else { + if ($Config.IsMultiTenant) { + foreach ($tenantKey in $Config.Tenants.Keys) { + Assert-DatabaseConnectionInfo -DbConnectionInfo $Config.Tenants[$tenantKey].AdminDbConnectionInfo -RequireDatabaseName + Assert-DatabaseConnectionInfo -DbConnectionInfo $Config.Tenants[$tenantKey].SecurityDbConnectionInfo -RequireDatabaseName + } + } + else{ Assert-DatabaseConnectionInfo -DbConnectionInfo $Config.AdminDbConnectionInfo - Assert-DatabaseConnectionInfo -DbConnectionInfo $Config.OdsDbConnectionInfo Assert-DatabaseConnectionInfo -DbConnectionInfo $Config.SecurityDbConnectionInfo - $Config.AdminDbConnectionInfo.ApplicationName = "Ed-Fi ODS/API AdminApi" - $Config.OdsDbConnectionInfo.ApplicationName = "Ed-Fi ODS/API AdminApi" - $Config.SecurityDbConnectionInfo.ApplicationName = "Ed-Fi ODS/API AdminApi" - $Config.engine = $Config.OdsDbConnectionInfo.Engine + $Config.AdminDbConnectionInfo.ApplicationName = "AdminApi" + $Config.SecurityDbConnectionInfo.ApplicationName = "AdminApi" + $Config.engine = $Config.AdminDbConnectionInfo.Engine + } } } } @@ -977,21 +953,9 @@ function Invoke-TransformAppSettings { Invoke-Task -Name ($MyInvocation.MyCommand.Name) -Task { $settingsFile = Join-Path $Config.WebConfigLocation "appsettings.json" $settings = Get-Content $settingsFile | ConvertFrom-Json | ConvertTo-Hashtable - $settings.AppSettings.OdsApiVersion = $Config.OdsApiVersion $settings.AppSettings.DatabaseEngine = $config.engine - if ($Config.AdminApiFeatures) { - if ($Config.AdminApiFeatures.ContainsKey("ApiMode") -and $Config.AdminApiFeatures.ApiMode) { - $settings.AppSettings.ApiStartupType = $Config.AdminApiFeatures.ApiMode - if ($Config.AdminApiFeatures.ApiMode -ieq "yearspecific" -or $Config.AdminApiFeatures.ApiMode -ieq "districtspecific") { - if (-not $Config.OdsDatabaseName.Contains("{0}")) { - $Config.OdsDatabaseName += "_{0}" - - $Config.OdsDatabaseName = $Config.OdsDatabaseName -replace "_Ods_\{0\}", "_{0}" - } - } - } - } + $settings.AppSettings.MultiTenancy = $config.IsMultiTenant $missingAuthenticationSettings = @() if ($Config.AuthenticationSettings.ContainsKey("Authority")) { @@ -1018,7 +982,6 @@ function Invoke-TransformAppSettings { $missingAuthenticationSettings += 'AllowRegistration' } - if ($missingAuthenticationSettings -gt 0) { Write-Warning "Please ensure all Admin Api authentication settings are configured correctly. The following Admin Api authentication settings are missing from the configuration: " foreach ($property in $missingAuthenticationSettings) { @@ -1044,9 +1007,6 @@ function Invoke-TransformConnectionStrings { $Config.AdminDbConnectionInfo = $Config.DbConnectionInfo.Clone() $Config.AdminDbConnectionInfo.DatabaseName = $Config.AdminDatabaseName - $Config.OdsDbConnectionInfo = $Config.DbConnectionInfo.Clone() - $Config.OdsDbConnectionInfo.DatabaseName = $Config.OdsDatabaseName - $Config.SecurityDbConnectionInfo = $Config.DbConnectionInfo.Clone() $Config.SecurityDbConnectionInfo.DatabaseName = $Config.SecurityDatabaseName } @@ -1055,9 +1015,6 @@ function Invoke-TransformConnectionStrings { if (-not $Config.AdminDbConnectionInfo.DatabaseName) { $Config.AdminDbConnectionInfo.DatabaseName = "EdFi_Admin" } - if (-not $Config.OdsDbConnectionInfo.DatabaseName) { - $Config.OdsDbConnectionInfo.DatabaseName = "EdFi_Ods" - } if (-not $Config.SecurityDbConnectionInfo.DatabaseName) { $Config.SecurityDbConnectionInfo.DatabaseName = "EdFi_Security" } @@ -1077,8 +1034,8 @@ function Invoke-TransformConnectionStrings { $connectionstrings = @{ ConnectionStrings = @{ - Admin = $adminconnString - Security = $securityConnString + EdFi_Admin = $adminconnString + EdFi_Security = $securityConnString } } @@ -1087,7 +1044,7 @@ function Invoke-TransformConnectionStrings { } } -function Get-AdminInstallConnectionString { +function Invoke-TransformMultiTenantConnectionStrings { [CmdletBinding()] param ( [hashtable] @@ -1095,8 +1052,62 @@ function Get-AdminInstallConnectionString { $Config ) + Invoke-Task -Name ($MyInvocation.MyCommand.Name) -Task { + # $webConfigPath = "$($Config.PackageDirectory)/appsettings.json" + # $settings = Get-Content $webConfigPath | ConvertFrom-Json | ConvertTo-Hashtable + + $settingsFile = Join-Path $Config.WebConfigLocation "appsettings.json" + $settings = Get-Content $settingsFile | ConvertFrom-Json | ConvertTo-Hashtable + + Write-Host "Setting database connections in $($Config.WebConfigLocation)" + + $newSettings = @{ + Tenants = @{} + } + + foreach ($tenantKey in $Config.Tenants.Keys) { + + if ($Config.usingSharedCredentials) { + $Config.Tenants[$tenantKey].AdminDbConnectionInfo = $Config.DbConnectionInfo.Clone() + $Config.Tenants[$tenantKey].AdminDbConnectionInfo.DatabaseName = $Config.Tenants[$tenantKey].AdminDatabaseName + + $Config.Tenants[$tenantKey].SecurityDbConnectionInfo = $Config.DbConnectionInfo.Clone() + $Config.Tenants[$tenantKey].SecurityDbConnectionInfo.DatabaseName = $Config.Tenants[$tenantKey].SecurityDatabaseName + } + + $adminconnString = New-ConnectionString -ConnectionInfo $Config.Tenants[$tenantKey].AdminDbConnectionInfo -SspiUsername $Config.WebApplicationName + $securityConnString = New-ConnectionString -ConnectionInfo $Config.Tenants[$tenantKey].SecurityDbConnectionInfo -SspiUsername $Config.WebApplicationName + + if ($Config.UnEncryptedConnection) { + $adminconnString += ";Encrypt=false" + $securityConnString += ";Encrypt=false" + } + + $newSettings.Tenants += @{ + $tenantKey = @{ + ConnectionStrings = @{ + EdFi_Admin = $adminconnString + EdFi_Security = $securityConnString + } + } + } + } + + $mergedSettings = Merge-Hashtables $settings, $newSettings + New-JsonFile $settingsFile $mergedSettings -Overwrite + } +} + +function Get-AdminInstallConnectionString { + [CmdletBinding()] + param ( + [hashtable] + [Parameter(Mandatory=$true)] + $AdminDbConnectionInfo + ) + $dbInstallCredentials = $Config.DatabaseInstallCredentials - $adminDbConnectionInfo = $Config.AdminDbConnectionInfo + $adminDbConnectionInfo = $AdminDbConnectionInfo $useInstallCredentials = ($dbInstallCredentials.UseIntegratedSecurity) -or ($dbInstallCredentials.DatabaseUser -and $dbInstallCredentials.DatabasePassword -and -not $dbInstallCredentials.UseIntegratedSecurity) @@ -1107,9 +1118,9 @@ function Get-AdminInstallConnectionString { } else { - if($Config.ApplicationInstallType -ieq "Upgrade" -and $Config.AdminConnectionString) + if($Config.ApplicationInstallType -ieq "Upgrade" -and $AdminDbConnectionInfo) { - return $Config.AdminConnectionString + return $AdminDbConnectionInfo } } @@ -1126,7 +1137,6 @@ function Invoke-DbUpScripts { Invoke-Task -Name ($MyInvocation.MyCommand.Name) -Task { - $adminConnectionString = Get-AdminInstallConnectionString $Config $engine = "PostgreSql" if(!(Test-IsPostgreSQL -Engine $Config.engine)){ @@ -1137,12 +1147,26 @@ function Invoke-DbUpScripts { Verb = "Deploy" Engine = $engine Database = "Admin" - ConnectionString = $adminConnectionString + ConnectionString = "" FilePaths = $Config.WebApplicationPath ToolsPath = $Config.ToolsPath } - Invoke-DbDeploy @params + if($Config.IsMultiTenant) + { + foreach ($tenantKey in $Config.Tenants.Keys) { + + $adminConnectionString = Get-AdminInstallConnectionString $Config.Tenants[$tenantKey].AdminDbConnectionInfo + $params["ConnectionString"] = $adminConnectionString + Invoke-DbDeploy @params + } + } + else + { + $adminConnectionString = Get-AdminInstallConnectionString $Config.AdminDbConnectionInfo + $params["ConnectionString"] = $adminConnectionString + Invoke-DbDeploy @params + } } } @@ -1206,14 +1230,22 @@ function Set-SqlLogins { } else { + if ($Config.IsMultiTenant) { + foreach ($tenantKey in $Config.Tenants.Keys) { + if ($Config.UseAlternateUserName ) { Write-Host ""; Write-Host "Adding Sql Login for Admin Database:"; } + Add-SqlLogins $Config.Tenants[$tenantKey].AdminDbConnectionInfo $Config.WebApplicationName -IsCustomLogin:$Config.UseAlternateUserName + + if ($Config.UseAlternateUserName ) { Write-Host ""; Write-Host "Adding Sql Login for Security Database:"; } + Add-SqlLogins $Config.Tenants[$tenantKey].SecurityDbConnectionInfo $Config.WebApplicationName -IsCustomLogin:$Config.UseAlternateUserName + } + } + else{ Write-Host "Adding Sql Login for Admin Database:"; Add-SqlLogins $Config.AdminDbConnectionInfo $Config.WebApplicationName -IsCustomLogin - Write-Host "Adding Sql Login for Ed-Fi ODS Database:"; - Add-SqlLogins $Config.OdsDbConnectionInfo $Config.WebApplicationName -IsCustomLogin - Write-Host "Adding Sql Login for Security Database:"; Add-SqlLogins $Config.SecurityDbConnectionInfo $Config.WebApplicationName -IsCustomLogin + } } } } diff --git a/Installer.AdminApi/install.ps1 b/Installer.AdminApi/install.ps1 index f73bb1e05..87f88bb20 100644 --- a/Installer.AdminApi/install.ps1 +++ b/Installer.AdminApi/install.ps1 @@ -40,35 +40,33 @@ $dbConnectionInfo = @{ Review and edit the following application settings and connection information for Admin App .EXAMPLE -Configure Admin Api to manage an ODS API with url "https://localhost:54746" +Configure Admin Api with Single tenant $p = @{ ToolsPath = "C:/temp/tools" DbConnectionInfo = $dbConnectionInfo - OdsApiVersion = "6.2" - PackageVersion = '1.4.3.0' - } - -.EXAMPLE -Deploy Admin Api for use with a "District Specific" ODS API - - $adminApiFeatures = @{ - ApiMode = "districtspecifc" + PackageVersion = "__ADMINAPI_VERSION__" } +Configure Admin Api with Multi tenant $p = @{ + IsMultiTenant = $true ToolsPath = "C:/temp/tools" DbConnectionInfo = $dbConnectionInfo - OdsApiVersion = "6.2" - PackageVersion = '1.4.3.0' - AdminApiFeatures = $adminApiFeatures + PackageVersion = "__ADMINAPI_VERSION__" + Tenants = @{ + Tenant1 = @{ + AdminDatabaseName = "EdFi_Admin_Tenant1" + SecurityDatabaseName = "EdFi_Security_Tenant1" + } + Tenant2 = @{ + AdminDatabaseName = "EdFi_Admin_Tenant2" + SecurityDatabaseName = "EdFi_Security_Tenant2" + } + } } #> -$adminApiFeatures = @{ - ApiMode = "sharedinstance" -} - # Authentication Settings # Authentication:SigningKey must be a Base64-encoded string # Authentication:Authority and Authentication:IssuerUrl should be the same URL as your application @@ -86,17 +84,12 @@ $adminApiSource = "$packageSource/AdminApi" $p = @{ ToolsPath = "C:/temp/tools" DbConnectionInfo = $dbConnectionInfo - OdsApiVersion = "" PackageVersion = "__ADMINAPI_VERSION__" PackageSource = $adminApiSource AuthenticationSettings = $authenticationSettings - AdminApiFeatures = $adminApiFeatures } -if ([string]::IsNullOrWhiteSpace($p.OdsApiVersion)) { - Write-Error "ODS API Version has not been configured. Edit install.ps1 to pass in a valid version number for the ODS API. Valid versions are 5.3, 5.3-cqe, 6.0 and 6.1" -} -elseif ([string]::IsNullOrWhiteSpace($p.AuthenticationSettings.Authority) -or [string]::IsNullOrWhiteSpace($p.AuthenticationSettings.IssuerUrl) -or [string]::IsNullOrWhiteSpace($p.AuthenticationSettings.SigningKey) -or $p.AuthenticationSettings.AllowRegistration -isnot [bool]) { +if ([string]::IsNullOrWhiteSpace($p.AuthenticationSettings.Authority) -or [string]::IsNullOrWhiteSpace($p.AuthenticationSettings.IssuerUrl) -or [string]::IsNullOrWhiteSpace($p.AuthenticationSettings.SigningKey) -or $p.AuthenticationSettings.AllowRegistration -isnot [bool]) { Write-Error "Authentication Settings have not been configured correctly. Edit install.ps1 to pass in valid authentication settings for Admin Api." } else { diff --git a/Installer.AdminApi/upgrade.ps1 b/Installer.AdminApi/upgrade.ps1 deleted file mode 100644 index beb6e92be..000000000 --- a/Installer.AdminApi/upgrade.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Licensed to the Ed-Fi Alliance under one or more agreements. -# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -# See the LICENSE and NOTICES files in the project root for more information. - -import-module -force "$PSScriptRoot/Install-AdminApi.psm1" - -<# -This script will take your existing Admin Api installation and upgrade it to the version indicated by PackageVersion below. -Your existing appsettings.json config values and connection strings will be copied forward to the new version. - -.EXAMPLE - $p = @{ - ToolsPath = "C:/temp/tools" - PackageVersion = '1.1.0' - OdsApiVersion = "6.1" -} -#> -$packageSource = Split-Path $PSScriptRoot -Parent -$adminApiSource = "$packageSource/AdminApi" - -$p = @{ - ToolsPath = "C:/temp/tools" - PackageVersion = '1.1.0.0' - PackageSource = $adminApiSource - OdsApiVersion = "" -} - -Update-EdFiOdsAdminApi @p - diff --git a/README.md b/README.md index a78987580..cf7824dd7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,16 @@ # Ed-Fi-ODS-AdminAPI -[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Ed-Fi-Alliance-OSS/AdminAPI-1.x/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Ed-Fi-Alliance-OSS/AdminAPI-1.x) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Ed-Fi-Alliance-OSS/AdminAPI-2.x/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Ed-Fi-Alliance-OSS/AdminAPI-2.x) The Ed-Fi ODS/API Admin API is a programmatic interface to administrate ODS/API -platform instances. For Admin API 1.2, it will support ODS/API versions 3.4 -through 6.1. +platform instances. Admin API 2.0 will support ODS/API version 7.0 and greater. +For support of ODS/API 3.4 through 6.1, please use the +[latest 1.x release](https://github.com/Ed-Fi-Alliance-OSS/Ed-Fi-AdminAPI/releases). + +> **Note** +> This is a fork of the [Ed-Fi-AdminAPI](https://github.com/Ed-Fi-Alliance-OSS/Ed-Fi-AdminAPI) +> repository, to enable continued development of Admin API 1.x. This fork may be +> merged back into the original repository at a later date. For more information, see: @@ -36,7 +42,7 @@ knowledge of the code base and architecture. ## Legal Information -Copyright (c) 2024 Ed-Fi Alliance, LLC and contributors. +Copyright (c) 2023 Ed-Fi Alliance, LLC and contributors. Licensed under the [Apache License, Version 2.0](LICENSE) (the "License"). diff --git a/SECURITY.md b/SECURITY.md index a65629143..48f578a09 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ privately. We prefer that you use the [GitHub mechanism for privately reporting a vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability). Under the [main repository's security -tab](https://github.com/Ed-Fi-Alliance-OSS/AdminAPI-1.x/security), click "Report a +tab](https://github.com/Ed-Fi-Alliance-OSS/AdminAPI-2.x/security), click "Report a vulnerability" to open the advisory form. If you have any further concerns that are not addressed by this process, please diff --git a/build.ps1 b/build.ps1 old mode 100644 new mode 100755 index 22c585179..5c4504de8 --- a/build.ps1 +++ b/build.ps1 @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: Apache-2.0 # Licensed to the Ed-Fi Alliance under one or more agreements. # The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. # See the LICENSE and NOTICES files in the project root for more information. @@ -48,33 +48,43 @@ .EXAMPLE $p = @{ - ProductionApiUrl = "http://api" - AppStartup = "OnPrem" - XsdFolder = "/app/Schema" - ApiStartupType = "SharedInstance" - DatabaseEngine = "PostgreSql" - BulkUploadHashCache = "/app/BulkUploadHashCache/" - EncryptionKey = "" - AdminDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Admin;Application Name=EdFi.Ods.AdminApi;" - SecurityDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Security;Application Name=EdFi.Ods.AdminApi;" + Authority = "http://api" + IssuerUrl = "https://localhost:5001" + DatabaseEngine = "PostgreSql" + PathBase = "adminapi" + SigningKey = "" + AdminDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Admin;Application Name=EdFi.Ods.AdminApi;" + SecurityDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Security;Application Name=EdFi.Ods.AdminApi;" } - .\build.ps1 -APIVersion "1.2.2" -Configuration Release -DockerEnvValues $p -Command BuildAndDeployToAdminApiDockerContainer + .\build.ps1 -APIVersion "2.2.0" -Configuration Release -DockerEnvValues $p -Command BuildAndDeployToAdminApiDockerContainer + .EXAMPLE + $p = @{ + Authority = "http://api" + IssuerUrl = "https://localhost" + DatabaseEngine = "PostgreSql" + PathBase = "adminapi" + SigningKey = "test" + AdminDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Admin;Application Name=EdFi.Ods.AdminApi;" + SecurityDB = "host=db-admin;port=5432;username=username;password=password;database=EdFi_Security;Application Name=EdFi.Ods.AdminApi;" + } + + ./build.ps1 -APIVersion ${{ inputs.version }} -Configuration Release -DockerEnvValues $p -Command GenerateOpenAPIAndMD + #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'False positive')] param( # Command to execute, defaults to "Build". [string] - [ValidateSet("Clean", "Build", "BuildAndPublish", "UnitTest", "IntegrationTest", "PackageApi" - , "Push", "BuildAndTest", "BuildAndDeployToAdminApiDockerContainer" - , "BuildAndRunAdminApiDevDocker", "RunAdminApiDevDockerContainer", "RunAdminApiDevDockerCompose", "Run", - "CopyApplicationFilesToDockerContext", "RemoveApplicationFilesFromDockerContext")] + [ValidateSet("Clean", "Build", "GenerateOpenAPIAndMD", "BuildAndPublish", "UnitTest", "IntegrationTest", "PackageApi" + , "Push", "BuildAndTest", "BuildAndDeployToAdminApiDockerContainer" + , "BuildAndRunAdminApiDevDocker", "RunAdminApiDevDockerContainer", "RunAdminApiDevDockerCompose", "Run", "CopyToDockerContext", "RemoveDockerContextFiles")] $Command = "Build", # Assembly and package version number for Admin API. The current package number is # configured in the build automation tool and passed to this script. [string] - $APIVersion = "1.2.2", + $APIVersion = "0.1", # .NET project build configuration, defaults to "Debug". Options are: Debug, Release. [string] @@ -109,7 +119,23 @@ param( # Only required with local builds and testing. [switch] - $IsLocalBuild + $IsLocalBuild, + + # Option to run coverlet for code coverage analysis, only applicable when running tests + [switch] + $RunCoverageAnalysis, + + # Option to run integration tests with or without integrated security + [Switch] + $UseIntegratedSecurity = $true, + + # Option to run integration tests with no integrated security + [string] + $DbUsername, + + # Option to run integration tests with no integrated security + [string] + $DbPassword ) $Env:MSBUILDDISABLENODEREUSE = "1" @@ -117,16 +143,22 @@ $Env:MSBUILDDISABLENODEREUSE = "1" $solutionRoot = "$PSScriptRoot/Application" $dockerRoot = "$PSScriptRoot/Docker" -$supportedApiVersions = @( +$supportedApiVersions7x = @( @{ - OdsPackageName = "EdFi.RestApi.Databases.EFA" - OdsVersion = "3.4.0" - Prerelease = $false - }, + OdsPackageName = "EdFi.Suite3.RestApi.Databases.Standard.5.2.0" + OdsVersion = "7.3.10536" + Prerelease = $false + StandardVersion = "5.2.0" + DbDeployVersion = "4.1.52" + } +) +$supportedApiVersions6x = @( @{ OdsPackageName = "EdFi.Suite3.RestApi.Databases" - OdsVersion = "5.3.659" - Prerelease = $false + OdsVersion = "6.2.3630" + Prerelease = $false + StandardVersion = "4.0.0" # v6.2 uses Db.Deploy 3.2.27, version 4.1.52 is for ODS 7.x. + DbDeployVersion = "3.2.27" } ) $maintainers = "Ed-Fi Alliance, LLC and contributors" @@ -134,6 +166,12 @@ $maintainers = "Ed-Fi Alliance, LLC and contributors" $appCommonPackageName = "EdFi.Installer.AppCommon" $appCommonPackageVersion = "3.0.0" +# Code coverage analysis +$script:coverageOutputFile = "coverage.cobertura.xml" +$script:targetDir = "coveragereport" + +$script:RunCoverageAnalysis = $RunCoverageAnalysis + Import-Module -Name "$PSScriptRoot/eng/build-helpers.psm1" -Force Import-Module -Name "$PSScriptRoot/eng/package-manager.psm1" -Force Import-Module -Name "$PSScriptRoot/eng/database-manager.psm1" -Force @@ -174,6 +212,29 @@ function Compile { } } +function GenerateOpenAPI { + Invoke-Execute { + Push-Location $solutionRoot/EdFi.Ods.AdminApi/ + $outputOpenAPI = "../../docs/api-specifications/openapi-yaml/admin-api-$APIVersion.yaml" + $dllPath = "./bin/Release/net8.0/EdFi.Ods.AdminApi.dll" + + try { + dotnet tool run swagger tofile --output $outputOpenAPI --yaml $dllPath v2 + } + finally { + Pop-Location + } + } +} + +function GenerateDocumentation { + Invoke-Execute { + $outputOpenAPI = "docs/api-specifications/openapi-yaml/admin-api-$APIVersion.yaml" + $outputMD = "docs/api-specifications/markdown/admin-api-$APIVersion-summary.md" + widdershins --search false --omitHeader true --code true --summary $outputOpenAPI -o $outputMD + } +} + function PublishAdminApi { Invoke-Execute { $project = "$solutionRoot/EdFi.Ods.AdminApi/" @@ -189,8 +250,8 @@ function RunTests { $Filter ) - $testAssemblyPath = "$solutionRoot/$Filter/bin/$Configuration/" - $testAssemblies = Get-ChildItem -Path $testAssemblyPath -Filter "$Filter.dll" -Recurse + $testAssemblyPath = "$solutionRoot/$Filter/" + $testAssemblies = Get-ChildItem -Path $testAssemblyPath -Filter "$Filter.csproj" -Recurse if ($testAssemblies.Length -eq 0) { Write-Output "no test assemblies found in $testAssemblyPath" @@ -199,13 +260,29 @@ function RunTests { $testAssemblies | ForEach-Object { Write-Output "Executing: dotnet test $($_)" Invoke-Execute { - dotnet test $_ ` - --logger "trx;LogFileName=$($_).trx" ` - --nologo + if ($script:RunCoverageAnalysis) + { + & dotnet test $_ --collect 'XPlat Code Coverage' --logger "trx;LogFileName=$($_).trx" --nologo + } + else + { + & dotnet test $_ --logger "trx;LogFileName=$($_).trx" --nologo + } } } } +function GenerateCoverageReport { + param ( + [string] + $Filter = "**" + ) + + Invoke-Execute { + reportgenerator -reports:"Application/$Filter/TestResults/**/*.cobertura.xml" -targetdir:"$script:targetDir" -reporttypes:Html + } +} + function UnitTests { Invoke-Execute { RunTests -Filter "*.UnitTests" } } @@ -220,24 +297,49 @@ function ResetTestDatabases { $OdsVersion, [switch] - $Prerelease + $Prerelease, + + [switch] + $UseIntegratedSecurity, + + [string] + $DbUsername, + + [string] + $DbPassword, + + [string] + [Parameter(Mandatory=$true)] + $StandardVersion, + + [string] + [Parameter(Mandatory=$true)] + $DbDeployVersion ) Invoke-Execute { $arguments = @{ - RestApiPackageVersion = $OdsVersion - RestApiPackageName = $OdsPackageName - UseIntegratedSecurity = $true + RestApiPackageVersion = $OdsVersion + RestApiPackageName = $OdsPackageName + UseIntegratedSecurity = $UseIntegratedSecurity + StandardVersion = $StandardVersion RestApiPackagePrerelease = $Prerelease - NuGetFeed = $EdFiNuGetFeed + NuGetFeed = $EdFiNuGetFeed + DbUsername = $DbUsername + DbPassword = $DbPassword + DbDeployVersion = $DbDeployVersion } Invoke-PrepareDatabasesForTesting @arguments } } -function IntegrationTests { - Invoke-Execute { RunTests -Filter "*.DBTests" } +function IntegrationTests7x { + Invoke-Execute { RunTests -Filter "*AdminApi.DBTests" } +} + +function IntegrationTests6x { + Invoke-Execute { RunTests -Filter "*AdminApi.V1.DBTests" } } function RunNuGetPack { @@ -255,7 +357,9 @@ function RunNuGetPack { # NU5100 is the warning about DLLs outside of a "lib" folder. We're # deliberately using that pattern, therefore we don't care about the # warning. - dotnet pack $ProjectPath --output $PSScriptRoot -p:NuspecFile=$nuspecPath -p:NuspecProperties="version=$PackageVersion" /p:NoWarn=NU5100 + # NU5110 is the warning about ps1 files outside the "tools" folder + # NU5111 is a warning about an unrecognized ps1 filename + dotnet pack $ProjectPath --output $PSScriptRoot -p:NuspecFile=$nuspecPath -p:NuspecProperties="version=$PackageVersion" /p:NoWarn='"NU5100;NU5110;NU5111"' } function NewDevCertificate { @@ -276,10 +380,10 @@ function AddAppCommonPackageForInstaller { $destinationPath = "$mainPath/publish" $arguments = @{ - AppCommonPackageName = $appCommonPackageName + AppCommonPackageName = $appCommonPackageName AppCommonPackageVersion = $appCommonPackageVersion - NuGetFeed = $EdFiNuGetFeed - DestinationPath = $destinationPath + NuGetFeed = $EdFiNuGetFeed + DestinationPath = $destinationPath } Add-AppCommon @arguments @@ -300,6 +404,15 @@ function Invoke-Build { Invoke-Step { Compile } } +function Invoke-GenerateOpenAPIAndMD { + Invoke-Step { UpdateAppSettingsForAdminApi } + Invoke-Step { DotNetClean } + Invoke-Step { Restore } + Invoke-Step { Compile } + Invoke-Step { GenerateOpenAPI } + Invoke-Step { GenerateDocumentation } +} + function Invoke-SetAssemblyInfo { Write-Output "Setting Assembly Information" @@ -321,8 +434,8 @@ function Invoke-Run { if ([string]::IsNullOrEmpty($LaunchProfile)) { Write-Error "LaunchProfile parameter is required for running Admin Api. Please " + - "specify the LaunchProfile parameter. Valid values include ""EdFi.Ods.AdminApi (Dev)""" + - ", ""EdFi.Ods.AdminApi (Prod)"", ""EdFi.Ods.AdminApi (Docker)"", and ""IIS Express""" + "specify the LaunchProfile parameter. Valid values include ""EdFi.Ods.AdminApi (Dev)""" + + ", ""EdFi.Ods.AdminApi (Prod)"", ""EdFi.Ods.AdminApi (Docker)"", and ""IIS Express""" } else { Invoke-Execute { dotnet run --project $projectFilePath --launch-profile $LaunchProfile } @@ -338,21 +451,58 @@ function Invoke-UnitTestSuite { } function Invoke-IntegrationTestSuite { - Invoke-Step { InitializeNuGet } + param ( + [Switch] + $UseIntegratedSecurity, + + [string] + $DbUsername, + + [string] + $DbPassword + ) + + $supportedApiVersions7x | ForEach-Object { + Write-Output "Running Integration Tests for ODS Version" $_.OdsVersion + + Invoke-Step { + $arguments = @{ + OdsVersion = $_.OdsVersion + OdsPackageName = $_.OdsPackageName + Prerelease = $_.Prerelease + StandardVersion = $_.StandardVersion + DbDeployVersion = $_.DbDeployVersion + UseIntegratedSecurity = $UseIntegratedSecurity + DbUsername = $DbUsername + DbPassword = $DbPassword + } + + ResetTestDatabases @arguments + } + Invoke-Step { + IntegrationTests7x + } + } - $supportedApiVersions | ForEach-Object { + $supportedApiVersions6x | ForEach-Object { Write-Output "Running Integration Tests for ODS Version" $_.OdsVersion Invoke-Step { $arguments = @{ - OdsVersion = $_.OdsVersion - OdsPackageName = $_.OdsPackageName - Prerelease = $_.Prerelease + OdsVersion = $_.OdsVersion + OdsPackageName = $_.OdsPackageName + Prerelease = $_.Prerelease + StandardVersion = $_.StandardVersion + DbDeployVersion = $_.DbDeployVersion + UseIntegratedSecurity = $UseIntegratedSecurity + DbUsername = $DbUsername + DbPassword = $DbPassword } + ResetTestDatabases @arguments } Invoke-Step { - IntegrationTests + IntegrationTests6x } } } @@ -369,17 +519,30 @@ function Invoke-BuildDatabasePackage { function UpdateAppSettingsForAdminApiDocker { $filePath = "$solutionRoot/EdFi.Ods.AdminApi/appsettings.json" $json = (Get-Content -Path $filePath) | ConvertFrom-Json - $json.AppSettings.ProductionApiUrl = $DockerEnvValues["ProductionApiUrl"] - $json.AppSettings.ApiStartupType = $DockerEnvValues["ApiStartupType"] $json.AppSettings.DatabaseEngine = $DockerEnvValues["DatabaseEngine"] $json.AppSettings.PathBase = $DockerEnvValues["PathBase"] $json.Authentication.IssuerUrl = $DockerEnvValues["IssuerUrl"] $json.Authentication.SigningKey = $DockerEnvValues["SigningKey"] - $json.ConnectionStrings.Admin = $DockerEnvValues["AdminDB"] - $json.ConnectionStrings.Security = $DockerEnvValues["SecurityDB"] - $json.Log4NetCore.Log4NetConfigFileName = "./log4net.config" + $json.ConnectionStrings.EdFi_Admin = $DockerEnvValues["AdminDB"] + $json.ConnectionStrings.EdFi_Security = $DockerEnvValues["SecurityDB"] + $json.Log4NetCore.Log4NetConfigFileName = "./log4net.config" + $json | ConvertTo-Json -Depth 10 | Set-Content $filePath +} + +function UpdateAppSettingsForAdminApi { + $filePath = "$solutionRoot/EdFi.Ods.AdminApi/appsettings.json" + $json = (Get-Content -Path $filePath) | ConvertFrom-Json + $json.AppSettings.DatabaseEngine = $DockerEnvValues["DatabaseEngine"] + $json.AppSettings.PathBase = $DockerEnvValues["PathBase"] + + $json.Authentication.IssuerUrl = $DockerEnvValues["IssuerUrl"] + $json.Authentication.SigningKey = $DockerEnvValues["SigningKey"] + + $json.ConnectionStrings.EdFi_Admin = $DockerEnvValues["AdminDB"] + $json.ConnectionStrings.EdFi_Security = $DockerEnvValues["SecurityDB"] + $json.Log4NetCore.Log4NetConfigFileName = "log4net/log4net.config" $json | ConvertTo-Json -Depth 10 | Set-Content $filePath } @@ -393,11 +556,31 @@ function RestartAdminApiContainer { } function BuildAdminApiDevDockerImage { - &docker build -t adminapi-dev --no-cache -f "$dockerRoot/dev.pgsql.Dockerfile" . + Push-Location $dockerRoot + try { + ">>> Building dev.pgsql.Dockerfile" | Out-Host + &docker build ` + -t adminapi-dev-pgsql ` + --build-context assets=$(Resolve-Path "..") ` + --no-cache ` + -f "dev.pgsql.Dockerfile" ` + . + + ">>> Building dev.mssql.Dockerfile" | Out-Host + &docker build ` + -t adminapi-dev-mssql ` + --build-context assets=$(Resolve-Path "..") ` + --no-cache ` + -f "dev.mssql.Dockerfile" ` + . + } + finally { + Pop-Location + } } function RunAdminApiDevDockerContainer { - &docker run --env-file "$solutionRoot/EdFi.Ods.AdminApi/.env" -p 80:80 -v "$dockerRoot/Settings/ssl:/ssl/" adminapi-dev + &docker run --env-file "$solutionRoot/EdFi.Ods.AdminApi/.env" -p 80:80 -v "$dockerRoot/Settings/ssl:/ssl/" adminapi-dev-pgsql } function RunAdminApiDevDockerCompose { @@ -416,76 +599,82 @@ function PushPackage { $arguments = @{ PackageFile = $PackageFile NuGetApiKey = $NuGetApiKey - NuGetFeed = $EdFiNuGetFeed + NuGetFeed = $EdFiNuGetFeed } Invoke-Execute { Push-Package @arguments } } - function Invoke-AdminApiDockerDeploy { - Invoke-Step { UpdateAppSettingsForAdminApiDocker } - Invoke-Step { CopyLatestFilesToAdminApiContainer } - Invoke-Step { RestartAdminApiContainer } + Invoke-Step { UpdateAppSettingsForAdminApiDocker } + Invoke-Step { CopyLatestFilesToAdminApiContainer } + Invoke-Step { RestartAdminApiContainer } } function Invoke-BuildAdminApiDevDockerImage { - Invoke-Step { BuildAdminApiDevDockerImage } + Invoke-Step { BuildAdminApiDevDockerImage } } function Invoke-RunAdminApiDevDockerContainer { - Invoke-Step { RunAdminApiDevDockerContainer } + Invoke-Step { RunAdminApiDevDockerContainer } } function Invoke-RunAdminApiDevDockerCompose { - Invoke-Step { RunAdminApiDevDockerCompose } + Invoke-Step { RunAdminApiDevDockerCompose } } function Invoke-PushPackage { Invoke-Step { PushPackage } } -function CopyApplicationFilesToDockerContext { - New-Item -Path "$dockerRoot/Application" -ItemType Directory - Copy-Item -Path "$solutionRoot/EdFi.Ods.AdminApi/" -Destination "$dockerRoot/Application/" -Recurse - Copy-Item -Path "$solutionRoot/NuGet.Config" -Destination "$dockerRoot/Application/" -Recurse -} - -function Invoke-CopyApplicationFilesToDockerContext { - Invoke-Step { CopyApplicationFilesToDockerContext } -} - -function RemoveApplicationFilesFromDockerContext { - $destinationApplication = "$dockerRoot/Application/" - if (Test-Path $destinationApplication) { - Remove-Item $destinationApplication -Recurse -Force - } -} - -function Invoke-RemoveApplicationFilesFromDockerContext { - Invoke-Step { RemoveApplicationFilesFromDockerContext } -} - Invoke-Main { - if($IsLocalBuild) - { + if ($IsLocalBuild) { $nugetExePath = Install-NugetCli Set-Alias nuget $nugetExePath -Scope Global -Verbose } switch ($Command) { Clean { Invoke-Clean } Build { Invoke-Build } + GenerateOpenAPIAndMD { Invoke-GenerateOpenAPIAndMD } BuildAndPublish { Invoke-SetAssemblyInfo Invoke-Build Invoke-Publish } Run { Invoke-Run } - UnitTest { Invoke-UnitTestSuite } - IntegrationTest { Invoke-IntegrationTestSuite } + UnitTest { + Invoke-UnitTestSuite + + if ($script:RunCoverageAnalysis) { + Invoke-Step { GenerateCoverageReport } + } + } + IntegrationTest { + $arguments = @{ + UseIntegratedSecurity = $UseIntegratedSecurity + DbUsername = $DbUsername + DbPassword = $DbPassword + } + + Invoke-IntegrationTestSuite @arguments + + if ($script:RunCoverageAnalysis) { + Invoke-Step { GenerateCoverageReport } + } + } BuildAndTest { + $arguments = @{ + UseIntegratedSecurity = $UseIntegratedSecurity + DbUsername = $DbUsername + DbPassword = $DbPassword + } + Invoke-Build Invoke-UnitTestSuite - Invoke-IntegrationTestSuite + Invoke-IntegrationTestSuite @arguments + + if ($script:RunCoverageAnalysis) { + Invoke-Step { GenerateCoverageReport } + } } Package { Invoke-BuildPackage } PackageApi { Invoke-BuildApiPackage } @@ -494,23 +683,16 @@ Invoke-Main { Invoke-Build Invoke-AdminApiDockerDeploy } - BuildAndRunAdminApiDevDocker{ + BuildAndRunAdminApiDevDocker { Invoke-BuildAdminApiDevDockerImage Invoke-RunAdminApiDevDockerContainer } - RunAdminApiDevDockerContainer{ + RunAdminApiDevDockerContainer { Invoke-RunAdminApiDevDockerContainer } - RunAdminApiDevDockerCompose{ + RunAdminApiDevDockerCompose { Invoke-RunAdminApiDevDockerCompose } - CopyApplicationFilesToDockerContext{ - Invoke-RemoveApplicationFilesFromDockerContext - Invoke-CopyApplicationFilesToDockerContext - } - RemoveApplicationFilesFromDockerContext{ - Invoke-RemoveApplicationFilesFromDockerContext - } default { throw "Command '$Command' is not recognized" } } -} +} \ No newline at end of file diff --git a/docs/ADMINAPI-1221.md b/docs/ADMINAPI-1221.md new file mode 100644 index 000000000..9da7966f5 --- /dev/null +++ b/docs/ADMINAPI-1221.md @@ -0,0 +1,76 @@ +# ADMINAPI-1221: `/applications` Endpoint Documentation + +## Purpose + +The `/applications` endpoint allows clients to retrieve one or more application resources. The API supports both legacy and new query patterns to maximize compatibility and flexibility for field usage. + +## How It Works + +* **HTTP Method:** `GET` +* **Route:** `/applications` (with query parameters) +* **Query Parameters:** + * `id` (single integer, legacy/field usage) + * `ids` (comma-separated list of integers, official batch retrieval) +* **Response:** + * For `id`, an array with a single `ApplicationModel` object is returned. + * For `ids`, an array of `ApplicationModel` objects is returned. +* **Error Handling:** + * If `id` is present and is not a valid integer, the endpoint will log the error to the console. + * If `ids` is present and is not a comma-separated list of integers, the endpoint returns a type error (400 Bad Request). + * If both `id` and `ids` are present, `id` takes precedence and only the single application for that `id` is returned. + * If neither parameter is present, all applications are returned (default behavior). + * If none of the provided IDs match existing applications, the endpoint returns a `404 Not Found`. + +### Example Requests + +```http +GET /applications?id=1 +GET /applications?ids=1,2,3 +``` + +### Example Responses + +```json +// For id=1 +[ + { + "id": 1, + "applicationName": "App One", + ... + } +] + +// For ids=1,2,3 +[ + { + "id": 1, + "applicationName": "App One", + ... + }, + { + "id": 2, + "applicationName": "App Two", + ... + } +] +``` + +## RESTful Alignment + +This approach aligns with REST principles by: + +* Using the `GET` method for safe, idempotent, read-only operations. +* Returning a single resource for `/applications?id=` and a collection for `/applications?ids=`. +* Using query parameters for filtering and batch retrieval, a common RESTful pattern. +* Not overloading the single-resource endpoint (`/applications/{id}`), keeping URIs predictable and semantically clear. + +## Why Support Both `id` and `ids`? + +We are keeping both ways. `applications?id=` is not official, but it is being used by our field so we don't want to modify or break existing integrations. Supporting both `id` and `ids` allows for a smooth transition and maximum compatibility. + +* The `id` parameter overrides the `ids` parameter if both are set. +* The `id` parameter must be a single integer. If a non-integer or a comma-separated list is passed (e.g., `id=1,2,3`), a type error is returned. + +## Summary + +The `/applications` endpoint provides a RESTful, non-breaking way to support both single and batch retrieval of applications. This approach keeps the API clean, predictable, backward compatible, and flexible for both legacy and new client needs. diff --git a/docs/Healthcheck-Worker-Removal.md b/docs/Healthcheck-Worker-Removal.md new file mode 100644 index 000000000..479c0f36c --- /dev/null +++ b/docs/Healthcheck-Worker-Removal.md @@ -0,0 +1,50 @@ +# Healthcheck Worker Removal Documentation + +## Overview + +This document outlines the changes made to remove the Healthcheck Worker endpoints from the Admin API as part of ticket ADMINAPI-1295. These changes were implemented to streamline the API and remove features that were no longer needed. + +## Components Removed + +The following components were removed from the codebase: + +1. **Healthcheck Worker Endpoints**: + * Healthcheck worker-specific endpoints in the Admin API + * Associated route registrations and handlers + +2. **Scopes**: + * Any specialized scopes related to healthcheck worker functionality + * Authorization policies specific to healthcheck workers + +## Restoring Functionality (If Needed) + +If the Healthcheck Worker functionality needs to be restored in the future, follow these steps: + +1. **Revert the Git Commit**: + + ```bash + git revert + ``` + + Where `` is the commit hash for the ADMINAPI-1295 changes. + +2. **Alternative: Cherry-Pick Previous Implementation**: + If you need to selectively restore parts of the healthcheck worker functionality: + + ```bash + git checkout -- Application/EdFi.Ods.AdminApi.AdminConsole/Features/Healthcheck/ + ``` + +## Testing After Restoration + +1. **API Functionality**: + * Test healthcheck worker endpoints + * Verify they respond with expected data + +2. **Integration Testing**: + * Verify that healthcheck worker functionality works properly with client applications + +## References + +* Ticket: ADMINAPI-1295 +* Commit: 5e3d484d2672c77b3ca459befbc490b140b43b41 diff --git a/docs/Instance-Worker-Removal.md b/docs/Instance-Worker-Removal.md new file mode 100644 index 000000000..b1db83802 --- /dev/null +++ b/docs/Instance-Worker-Removal.md @@ -0,0 +1,80 @@ +# Instance Management Worker Removal + +## Overview + +This document outlines the changes made to remove the Instance Worker endpoints from the Admin API as part of ticket ADMINAPI-1294. These changes were implemented to streamline the API and remove features that were no longer needed. + +## Components Removed + +The following components were removed from the codebase: + +1. **Instance Worker Endpoints**: + * Instance worker-specific endpoints in the Admin API + * Associated route registrations and handlers + * Endpoints included: + * `/adminconsole/odsInstances` (GET) - Used to list available ODS instances + * `/adminconsole/odsInstances/{id}` (GET) - Used to get specific ODS instance metadata + * `/adminconsole/odsInstances` (POST) - Used to create a new ODS instance request + * `/adminconsole/odsInstances` (PUT) - Used to update an existing ODS instance + * `/adminconsole/odsInstances` (DELETE) - Used to request deletion of an ODS instanceemoval + * `/adminconsole/instances` (GET) - Used to retrieve instances for worker + * `/adminconsole/instances/{id}` (GET) - Used to get specific instance details + * `/adminconsole/instances` (POST) - Used to create new instance + * `/adminconsole/instances` (PUT) - Used to update instance + * `/adminconsole/instances` (DELETE) - Used to delete instance + * `/adminconsole/instances/{instanceId}/completed` - Used to mark instance creation as completed + * `/adminconsole/instances/{instanceId}/deletefailed` - Used to mark instance deletion as failed + * `/adminconsole/instances/{instanceId}/renameFailed` - Used to mark instance rename as failed + * `/adminconsole/instances/{instanceId}/renamed` - Used to mark instance rename as completed + * `/adminconsole/instances/{instanceId}/deleted` - Used to mark instance deletion as completed + +2. **Supporting Code**: + * Instance worker models and DTOs (e.g., `InstanceWorkerModel`) + * Feature handlers (e.g., `WorkerInstanceRenamed`, `WorkerInstanceDeleted`, `WorkerInstanceRenameFailed`) + * Instance management services and commands: + * Instance creation/completion commands + * Instance deletion commands + * Instance rename commands + * Status transition handlers for instance lifecycle management + +3. **Database Components**: + * Tables: + * `adminconsole.Instances` - Stored instance management information and status tracking for the worker queue + * `adminconsole.OdsInstanceContexts` - Instance context mapping that duplicated information already in `dbo.OdsInstanceContext` + * `adminconsole.OdsInstanceDerivatives` - Instance derivative information that duplicated data in `dbo.OdsInstanceDerivative` + +These database tables primarily functioned as a job queue system for the Instance Management Worker, tracking the status of instance operations (creation, deletion, renaming). This information was largely redundant with the core data already maintained in the standard `dbo` schema tables, which ultimately contained the authoritative instance data used by the ODS/API. + +## Restoring Functionality (If Needed) + +If the Instance Worker functionality needs to be restored in the future, follow these steps: + +1. **Revert the Git Commit**: + + ```bash + git revert + ``` + + Where `` is the commit hash for the ADMINAPI-1294 changes. + +2. **Alternative: Cherry-Pick Previous Implementation**: + If you need to selectively restore parts of the instance worker functionality: + + ```bash + git checkout -- Application/EdFi.Ods.AdminApi.AdminConsole/Features/WorkerInstances/ + ``` + +## Testing After Restoration + +1. **API Functionality**: + * Test instance worker endpoints + * Verify they respond with expected data + +2. **Integration Testing**: + * Verify that instance worker functionality works properly with client applications + * Test the full lifecycle of instance creation, renaming, and deletion + +## References + +* Ticket: ADMINAPI-1294 +* Commit: [Include the commit hash once the changes are merged] diff --git a/docs/api-specifications/markdown/admin-api-2.2.0-summary.md b/docs/api-specifications/markdown/admin-api-2.2.0-summary.md new file mode 100644 index 000000000..341a29b1d --- /dev/null +++ b/docs/api-specifications/markdown/admin-api-2.2.0-summary.md @@ -0,0 +1,3906 @@ + + +

Admin API v2.2.0

+ +The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API. + +# Authentication + +- oAuth2 authentication. + + - Flow: clientCredentials + + - Token URL = [https://localhost/adminapi/connect/token](https://localhost/adminapi/connect/token) + +|Scope|Scope Description| +|---|---| +|edfi_admin_api/full_access|Unrestricted access to all Admin API endpoints| + +

ResourceClaims

+ +## Retrieves all resourceClaims. + +`GET /v2/resourceClaims` + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + {} + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[resourceClaim](#schemaresourceclaim)]|false|none|none| +|» ResourceClaimModel|[resourceClaim](#schemaresourceclaim)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» name|string¦null|true|none|none| +|»» parentId|integer(int32)¦null|true|none|none| +|»» parentName|string¦null|true|none|none| +|»» children|[[resourceClaim](#schemaresourceclaim)]¦null|true|none|Children are collection of SimpleResourceClaimModel| +|»»» ResourceClaimModel|[resourceClaim](#schemaresourceclaim)|false|none|none| + + + +## Retrieves a specific resourceClaim based on the identifier. + +`GET /v2/resourceClaims/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[resourceClaim](#schemaresourceclaim)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

Vendors

+ +## Retrieves all vendors. + +`GET /v2/vendors` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|true|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|true|Indicates the maximum number of items that should be returned in the results.| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[vendor](#schemavendor)]|false|none|none| +|» Vendor|[vendor](#schemavendor)|false|none|none| +|»» id|integer(int32)¦null|true|none|none| +|»» company|string¦null|true|none|none| +|»» namespacePrefixes|string¦null|true|none|none| +|»» contactName|string¦null|true|none|none| +|»» contactEmailAddress|string¦null|true|none|none| + + + +## Creates vendor based on the supplied values. + +`POST /v2/vendors` + +> Body parameter + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addVendorRequest](#schemaaddvendorrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific vendor based on the identifier. + +`GET /v2/vendors/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[vendor](#schemavendor)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates vendor based on the resource identifier. + +`PUT /v2/vendors/{id}` + +> Body parameter + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editVendorRequest](#schemaeditvendorrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing vendor using the resource identifier. + +`DELETE /v2/vendors/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves applications assigned to a specific vendor based on the resource identifier. + +`GET /v2/vendors/{id}/applications` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[application](#schemaapplication)]|false|none|none| +|» Application|[application](#schemaapplication)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» applicationName|string¦null|true|none|none| +|»» claimSetName|string¦null|true|none|none| +|»» educationOrganizationIds|[integer]¦null|true|none|none| +|»» vendorId|integer(int32)¦null|true|none|none| +|»» profileIds|[integer]¦null|true|none|none| +|»» odsInstanceIds|[integer]¦null|true|none|none| + + + +

Profiles

+ +## Retrieves all profiles. + +`GET /v2/profiles` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|true|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|true|Indicates the maximum number of items that should be returned in the results.| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[profile](#schemaprofile)]|false|none|none| +|» Profile|[profile](#schemaprofile)|false|none|none| +|»» id|integer(int32)¦null|true|none|none| +|»» name|string¦null|true|none|none| + + + +## Creates profile based on the supplied values. + +`POST /v2/profiles` + +> Body parameter + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addProfileRequest](#schemaaddprofilerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific profile based on the identifier. + +`GET /v2/profiles/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "definition": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[profileDetails](#schemaprofiledetails)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates profile based on the resource identifier. + +`PUT /v2/profiles/{id}` + +> Body parameter + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editProfileRequest](#schemaeditprofilerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing profile using the resource identifier. + +`DELETE /v2/profiles/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

OdsInstances

+ +## Retrieves all odsInstances. + +`GET /v2/odsInstances` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|true|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|true|Indicates the maximum number of items that should be returned in the results.| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "instanceType": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[odsInstance](#schemaodsinstance)]|false|none|none| +|» OdsInstance|[odsInstance](#schemaodsinstance)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|true|none|none| +|»» instanceType|string¦null|true|none|none| + + + +## Creates odsInstance based on the supplied values. + +`POST /v2/odsInstances` + +> Body parameter + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addOdsIntanceRequest](#schemaaddodsintancerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstance based on the identifier. + +`GET /v2/odsInstances/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string", + "odsInstanceContexts": [ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } + ], + "odsInstanceDerivatives": [ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[odsInstanceDetail](#schemaodsinstancedetail)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstance based on the resource identifier. + +`PUT /v2/odsInstances/{id}` + +> Body parameter + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editOdsInstanceRequest](#schemaeditodsinstancerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstance using the resource identifier. + +`DELETE /v2/odsInstances/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves applications assigned to a specific ODS instance based on the resource identifier. + +`GET /v2/odsInstances/{id}/applications` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[application](#schemaapplication)]|false|none|none| +|» Application|[application](#schemaapplication)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» applicationName|string¦null|true|none|none| +|»» claimSetName|string¦null|true|none|none| +|»» educationOrganizationIds|[integer]¦null|true|none|none| +|»» vendorId|integer(int32)¦null|true|none|none| +|»» profileIds|[integer]¦null|true|none|none| +|»» odsInstanceIds|[integer]¦null|true|none|none| + + + +

OdsInstanceDerivatives

+ +## Retrieves all odsInstanceDerivatives. + +`GET /v2/odsInstanceDerivatives` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|true|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|true|Indicates the maximum number of items that should be returned in the results.| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[odsInstanceDerivative](#schemaodsinstancederivative)]|false|none|none| +|» OdsInstanceDerivative|[odsInstanceDerivative](#schemaodsinstancederivative)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» odsInstanceId|integer(int32)¦null|true|none|none| +|»» derivativeType|string¦null|true|none|none| + + + +## Creates odsInstanceDerivative based on the supplied values. + +`POST /v2/odsInstanceDerivatives` + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addOdsInstanceDerivateRequest](#schemaaddodsinstancederivaterequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstanceDerivative based on the identifier. + +`GET /v2/odsInstanceDerivatives/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[odsInstanceDerivative](#schemaodsinstancederivative)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstanceDerivative based on the resource identifier. + +`PUT /v2/odsInstanceDerivatives/{id}` + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editOdsInstanceDerivateRequest](#schemaeditodsinstancederivaterequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstanceDerivative using the resource identifier. + +`DELETE /v2/odsInstanceDerivatives/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

OdsInstanceContexts

+ +## Retrieves all odsInstanceContexts. + +`GET /v2/odsInstanceContexts` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|true|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|true|Indicates the maximum number of items that should be returned in the results.| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[odsInstanceContext](#schemaodsinstancecontext)]|false|none|none| +|» OdsInstanceContext|[odsInstanceContext](#schemaodsinstancecontext)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» odsInstanceId|integer(int32)|true|none|none| +|»» contextKey|string¦null|true|none|none| +|»» contextValue|string¦null|true|none|none| + + + +## Creates odsInstanceContext based on the supplied values. + +`POST /v2/odsInstanceContexts` + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addOdsInstanceContextRequest](#schemaaddodsinstancecontextrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstanceContext based on the identifier. + +`GET /v2/odsInstanceContexts/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[odsInstanceContext](#schemaodsinstancecontext)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstanceContext based on the resource identifier. + +`PUT /v2/odsInstanceContexts/{id}` + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editOdsInstanceContextRequest](#schemaeditodsinstancecontextrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstanceContext using the resource identifier. + +`DELETE /v2/odsInstanceContexts/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

ClaimSets

+ +## Exports a specific claimSet based on the identifier. + +`GET /v2/claimSets/{id}/export` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[claimsetWithResources](#schemaclaimsetwithresources)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves all claimSets. + +`GET /v2/claimSets` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|true|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|true|Indicates the maximum number of items that should be returned in the results.| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[claimset](#schemaclaimset)]|false|none|none| +|» ClaimSet|[claimset](#schemaclaimset)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» name|string¦null|true|none|none| +|»» _isSystemReserved|boolean|false|read-only|none| +|»» _applications|[[simpleApplication](#schemasimpleapplication)]¦null|false|read-only|none| +|»»» Application|[simpleApplication](#schemasimpleapplication)|false|none|none| +|»»»» applicationName|string¦null|true|none|none| + + + +## Creates claimSet based on the supplied values. + +`POST /v2/claimSets` + +> Body parameter + +```json +{ + "name": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addClaimsetRequest](#schemaaddclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific claimSet based on the identifier. + +`GET /v2/claimSets/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[claimsetWithResources](#schemaclaimsetwithresources)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates claimSet based on the resource identifier. + +`PUT /v2/claimSets/{id}` + +> Body parameter + +```json +{ + "name": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editClaimsetRequest](#schemaeditclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing claimSet using the resource identifier. + +`DELETE /v2/claimSets/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Copies the existing claimset and create new one. + +`POST /v2/claimSets/copy` + +> Body parameter + +```json +{ + "originalId": 0, + "name": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[copyClaimsetRequest](#schemacopyclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Imports a new claimSet based on the supplied values. + +`POST /v2/claimSets/import` + +> Body parameter + +```json +{ + "name": "string", + "resourceClaims": [ + { + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[importClaimsetRequest](#schemaimportclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Overrides the default authorization strategies on provided resource claim for a specific action. + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/overrideAuthorizationStrategy` + +> Body parameter + +```json +{ + "actionName": "string", + "authorizationStrategies": [ + "string" + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| +|body|body|[overrideAuthorizationStrategyOnClaimsetRequest](#schemaoverrideauthorizationstrategyonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Resets to default authorization strategies on provided resource claim. + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/resetAuthorizationStrategies` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Adds resourceClaimAction association to a claimSet. + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions` + +> Body parameter + +```json +{ + "resourceClaimId": 0, + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|body|body|[addResourceClaimActionsOnClaimsetRequest](#schemaaddresourceclaimactionsonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates the resourceClaimAction to a specific resourceClaim on a claimSet. + +`PUT /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}` + +> Body parameter + +```json +{ + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| +|body|body|[editResourceClaimActionsOnClaimsetRequest](#schemaeditresourceclaimactionsonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes a resourceClaims association from a claimSet. + +`DELETE /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

AuthorizationStrategies

+ +## Retrieves all authorizationStrategies. + +`GET /v2/authorizationStrategies` + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "displayName": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[authorizationStrategy](#schemaauthorizationStrategy)]|false|none|none| +|» AuthorizationStrategy|[authorizationStrategy](#schemaauthorizationStrategy)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» displayName|string¦null|true|none|none| + + + +

Applications

+ +## Retrieves all applications. + +`GET /v2/applications` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|true|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|true|Indicates the maximum number of items that should be returned in the results.| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[application](#schemaapplication)]|false|none|none| +|» Application|[application](#schemaapplication)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» applicationName|string¦null|true|none|none| +|»» claimSetName|string¦null|true|none|none| +|»» educationOrganizationIds|[integer]¦null|true|none|none| +|»» vendorId|integer(int32)¦null|true|none|none| +|»» profileIds|[integer]¦null|true|none|none| +|»» odsInstanceIds|[integer]¦null|true|none|none| + + + +## Creates application based on the supplied values. + +`POST /v2/applications` + +> Body parameter + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addApplicationRequest](#schemaaddapplicationrequest)|true|none| + +> Example responses + +> 201 Response + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|[applicationResult](#schemaapplicationresult)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific application based on the identifier. + +`GET /v2/applications/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[application](#schemaapplication)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates application based on the resource identifier. + +`PUT /v2/applications/{id}` + +> Body parameter + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editApplicationResult](#schemaeditapplicationresult)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing application using the resource identifier. + +`DELETE /v2/applications/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Reset application credentials. Returns new key and secret. + +`PUT /v2/applications/{id}/reset-credential` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[applicationResult](#schemaapplicationresult)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

Actions

+ +## Retrieves all actions. + +`GET /v2/actions` + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "uri": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[action](#schemaaction)]|false|none|none| +|» Action|[action](#schemaaction)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» name|string¦null|true|none|none| +|»» uri|string¦null|true|none|none| + + + +

Information

+ +## Retrieve API informational metadata + +`GET /` + +> Example responses + +> 200 Response + +```json +{ + "version": "string", + "build": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[information](#schemainformation)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|[information](#schemainformation)| + + + +

Connect

+ +## Registers new client + +`POST /connect/register` + +Registers new client + +> Body parameter + +```yaml +ClientId: string +ClientSecret: string +DisplayName: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» ClientId|body|string|false|Client id| +|» ClientSecret|body|string|false|Client secret| +|» DisplayName|body|string|false|Client display name| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Application registered successfully.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves bearer token + +`POST /connect/token` + +To authenticate Swagger requests, execute using "Authorize" above, not "Try It Out" here. + +> Body parameter + +```yaml +client_id: string +client_secret: string +grant_type: string +scope: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» client_id|body|string|false|none| +|» client_secret|body|string|false|none| +|» grant_type|body|string|false|none| +|» scope|body|string|false|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Sign-in successful.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +# Schemas + +

action

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "uri": "string" +} + +``` + +Action + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|name|string¦null|true|none|none| +|uri|string¦null|true|none|none| + +

adminApiError

+ + + + + + +```json +{ + "title": "string", + "errors": [ + "string" + ] +} + +``` + +AdminApiError + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|title|string¦null|true|read-only|none| +|errors|[string]¦null|true|read-only|none| + +

addApplicationRequest

+ + + + + + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +AddApplicationRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string|true|none|Application name| +|vendorId|integer(int32)|true|none|Vendor/ company id| +|claimSetName|string|true|none|Claim set name| +|profileIds|[integer]¦null|false|none|Profile id| +|educationOrganizationIds|[integer]|true|none|Education organization ids| +|odsInstanceIds|[integer]|true|none|List of ODS instance id| + +

application

+ + + + + + +```json +{ + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +Application + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|applicationName|string¦null|true|none|none| +|claimSetName|string¦null|true|none|none| +|educationOrganizationIds|[integer]¦null|true|none|none| +|vendorId|integer(int32)¦null|true|none|none| +|profileIds|[integer]¦null|true|none|none| +|odsInstanceIds|[integer]¦null|true|none|none| + +

applicationResult

+ + + + + + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} + +``` + +ApplicationKeySecret + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|key|string¦null|true|none|none| +|secret|string¦null|true|none|none| + +

editApplicationResult

+ + + + + + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +EditApplicationRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string|true|none|Application name| +|vendorId|integer(int32)|true|none|Vendor/ company id| +|claimSetName|string|true|none|Claim set name| +|profileIds|[integer]¦null|false|none|Profile id| +|educationOrganizationIds|[integer]|true|none|Education organization ids| +|odsInstanceIds|[integer]|true|none|List of ODS instance id| + +

simpleApplication

+ + + + + + +```json +{ + "applicationName": "string" +} + +``` + +Application + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string¦null|true|none|none| + +

authorizationStrategy

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "displayName": "string" +} + +``` + +AuthorizationStrategy + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|displayName|string¦null|true|none|none| + +

addClaimsetRequest

+ + + + + + +```json +{ + "name": "string" +} + +``` + +AddClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Claim set name| + +

claimsetResourceClaim

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [] + } + ] + } + ] +} + +``` + +ClaimSetResourceClaim + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|read-only|none| +|name|string¦null|true|none|none| +|actions|[[resourceClaimAction](#schemaresourceclaimaction)]¦null|true|none|none| +|_defaultAuthorizationStrategiesForCRUD|[[claimsetResourceClaimActionAuthorizationStrategies](#schemaclaimsetresourceclaimactionauthorizationstrategies)]¦null|false|read-only|none| +|authorizationStrategyOverridesForCRUD|[[claimsetResourceClaimActionAuthorizationStrategies](#schemaclaimsetresourceclaimactionauthorizationstrategies)]¦null|true|none|none| +|children|[[claimsetResourcesClaim](#schemaclaimsetresourcesclaim)]¦null|true|none|Children are collection of ResourceClaim| + +

claimsetWithResources

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] + } + ] +} + +``` + +ClaimSetWithResources + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|name|string¦null|true|none|none| +|_isSystemReserved|boolean|false|read-only|none| +|_applications|[[simpleApplication](#schemasimpleapplication)]¦null|false|read-only|none| +|resourceClaims|[[claimsetResourcesClaim](#schemaclaimsetresourcesclaim)]¦null|true|none|none| + +

claimset

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ] +} + +``` + +ClaimSet + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|name|string¦null|true|none|none| +|_isSystemReserved|boolean|false|read-only|none| +|_applications|[[simpleApplication](#schemasimpleapplication)]¦null|false|read-only|none| + +

claimsetResourcesClaim

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [] + } + ] + } + ] +} + +``` + +ClaimSetResourceClaim + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|read-only|none| +|name|string¦null|true|none|none| +|actions|[[resourceClaimAction](#schemaresourceclaimaction)]¦null|true|none|none| +|_defaultAuthorizationStrategiesForCRUD|[[claimsetResourceClaimActionAuthorizationStrategies](#schemaclaimsetresourceclaimactionauthorizationstrategies)]¦null|false|read-only|none| +|authorizationStrategyOverridesForCRUD|[[claimsetResourceClaimActionAuthorizationStrategies](#schemaclaimsetresourceclaimactionauthorizationstrategies)]¦null|true|none|none| +|children|[[claimsetResourceClaim](#schemaclaimsetresourceclaim)]¦null|true|none|Children are collection of ResourceClaim| + +

copyClaimsetRequest

+ + + + + + +```json +{ + "originalId": 0, + "name": "string" +} + +``` + +CopyClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|originalId|integer(int32)|true|none|ClaimSet id to copy| +|name|string|true|none|New claimset name| + +

editClaimsetRequest

+ + + + + + +```json +{ + "name": "string" +} + +``` + +EditClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Claim set name| + +

importClaimsetRequest

+ + + + + + +```json +{ + "name": "string", + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] + } + ] +} + +``` + +ImportClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Claim set name| +|resourceClaims|[[claimsetResourcesClaim](#schemaclaimsetresourcesclaim)]|true|none|Resource Claims| + +

resourceClaim

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [] + } + ] +} + +``` + +ResourceClaimModel + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|name|string¦null|true|none|none| +|parentId|integer(int32)¦null|true|none|none| +|parentName|string¦null|true|none|none| +|children|[[resourceClaim](#schemaresourceclaim)]¦null|true|none|Children are collection of SimpleResourceClaimModel| + +

overrideAuthorizationStrategyOnClaimsetRequest

+ + + + + + +```json +{ + "actionName": "string", + "authorizationStrategies": [ + "string" + ] +} + +``` + +OverrideAuthStategyOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|actionName|string¦null|true|none|none| +|authorizationStrategies|[string]|true|none|AuthorizationStrategy Names| + +

addResourceClaimActionsOnClaimsetRequest

+ + + + + + +```json +{ + "resourceClaimId": 0, + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} + +``` + +AddResourceClaimActionsOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|resourceClaimId|integer(int32)|true|none|ResourceClaim id| +|resourceClaimActions|[[resourceClaimAction](#schemaresourceclaimaction)]|true|none|none| + +

editResourceClaimActionsOnClaimsetRequest

+ + + + + + +```json +{ + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} + +``` + +EditResourceClaimActionsOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|resourceClaimActions|[[resourceClaimAction](#schemaresourceclaimaction)]|true|none|none| + +

registerClientRequest

+ + + + + + +```json +{ + "clientId": "string", + "clientSecret": "string", + "displayName": "string" +} + +``` + +RegisterClientRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|clientId|string|true|none|Client id| +|clientSecret|string|true|none|Client secret| +|displayName|string|true|none|Client display name| + +

information

+ + + + + + +```json +{ + "version": "string", + "build": "string" +} + +``` + +Information + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|version|string|true|none|Application version| +|build|string|true|none|Build / release version| + +

odsInstanceDetail

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string", + "odsInstanceContexts": [ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } + ], + "odsInstanceDerivatives": [ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } + ] +} + +``` + +OdsInstanceDetail + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|true|none|none| +|instanceType|string¦null|true|none|none| +|odsInstanceContexts|[[odsInstanceContext](#schemaodsinstancecontext)]¦null|true|none|none| +|odsInstanceDerivatives|[[odsInstanceDerivative](#schemaodsinstancederivative)]¦null|true|none|none| + +

odsInstance

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string" +} + +``` + +OdsInstance + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|true|none|none| +|instanceType|string¦null|true|none|none| + +

addOdsInstanceContextRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +AddOdsInstanceContextRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|true|none|ODS instance context ODS instance id.| +|contextKey|string|true|none|context key.| +|contextValue|string|true|none|context value.| + +

editOdsInstanceContextRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +EditOdsInstanceContextRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|true|none|ODS instance context ODS instance id.| +|contextKey|string|true|none|context key.| +|contextValue|string|true|none|context value.| + +

odsInstanceContext

+ + + + + + +```json +{ + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +OdsInstanceContext + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|odsInstanceId|integer(int32)|true|none|none| +|contextKey|string¦null|true|none|none| +|contextValue|string¦null|true|none|none| + +

addOdsInstanceDerivateRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} + +``` + +AddOdsInstanceDerivativeRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|true|none|ODS instance derivative ODS instance id.| +|derivativeType|string|true|none|derivative type.| +|connectionString|string|true|none|connection string.| + +

editOdsInstanceDerivateRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} + +``` + +EditOdsInstanceDerivativeRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|true|none|ODS instance derivative ODS instance id.| +|derivativeType|string|true|none|derivative type.| +|connectionString|string|true|none|connection string.| + +

odsInstanceDerivative

+ + + + + + +```json +{ + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" +} + +``` + +OdsInstanceDerivative + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|odsInstanceId|integer(int32)¦null|true|none|none| +|derivativeType|string¦null|true|none|none| + +

addOdsIntanceRequest

+ + + + + + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} + +``` + +AddOdsInstanceRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Ods Instance name| +|instanceType|string|true|none|Ods Instance type| +|connectionString|string|true|none|Ods Instance connection string| + +

editOdsInstanceRequest

+ + + + + + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} + +``` + +EditOdsInstanceRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Ods Instance name| +|instanceType|string|true|none|Ods Instance type| +|connectionString|string¦null|true|none|Ods Instance connection string| + +

addProfileRequest

+ + + + + + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + +``` + +AddProfileRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Profile name| +|definition|string|true|none|Profile definition| + +

editProfileRequest

+ + + + + + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + +``` + +EditProfileRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Profile name| +|definition|string|true|none|Profile definition| + +

profileDetails

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "definition": "string" +} + +``` + +ProfileDetails + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|true|none|none| +|name|string¦null|true|none|none| +|definition|string¦null|true|none|none| + +

profile

+ + + + + + +```json +{ + "id": 0, + "name": "string" +} + +``` + +Profile + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|true|none|none| +|name|string¦null|true|none|none| + +

addVendorRequest

+ + + + + + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +AddVendorRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|company|string|true|none|Vendor/ company name| +|namespacePrefixes|string|true|none|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|string|true|none|Vendor contact name| +|contactEmailAddress|string|true|none|Vendor contact email id| + +

editVendorRequest

+ + + + + + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +EditVendorRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|company|string|true|none|Vendor/ company name| +|namespacePrefixes|string|true|none|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|string|true|none|Vendor contact name| +|contactEmailAddress|string|true|none|Vendor contact email id| + +

vendor

+ + + + + + +```json +{ + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +Vendor + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|true|none|none| +|company|string¦null|true|none|none| +|namespacePrefixes|string¦null|true|none|none| +|contactName|string¦null|true|none|none| +|contactEmailAddress|string¦null|true|none|none| + +

resourceClaimAuthorizationStrategy

+ + + + + + +```json +{ + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true +} + +``` + +ResourceClaimAuthorizationStrategy + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|authStrategyId|integer(int32)|true|none|none| +|authStrategyName|string¦null|true|none|none| +|isInheritedFromParent|boolean|true|none|none| + +

claimsetResourceClaimActionAuthorizationStrategies

+ + + + + + +```json +{ + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] +} + +``` + +ClaimSetResourceClaimActionAuthorizationStrategies + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|actionId|integer(int32)¦null|true|none|none| +|actionName|string¦null|true|none|none| +|authorizationStrategies|[[resourceClaimAuthorizationStrategy](#schemaresourceclaimauthorizationstrategy)]¦null|true|none|none| + +

resourceClaimAction

+ + + + + + +```json +{ + "name": "string", + "enabled": true +} + +``` + +ResourceClaimAction + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string¦null|true|none|none| +|enabled|boolean|true|none|none| + diff --git a/docs/api-specifications/markdown/admin-api-2.2.1-summary.md b/docs/api-specifications/markdown/admin-api-2.2.1-summary.md new file mode 100644 index 000000000..f87ff313b --- /dev/null +++ b/docs/api-specifications/markdown/admin-api-2.2.1-summary.md @@ -0,0 +1,3824 @@ + + +

Admin API Documentation v2.2.1

+ +> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu. + +The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API. + +# Authentication + +- oAuth2 authentication. + + - Flow: clientCredentials + + - Token URL = [http://localhost/connect/token](http://localhost/connect/token) + +|Scope|Scope Description| +|---|---| +|edfi_admin_api/full_access|Unrestricted access to all Admin API endpoints| + +

ResourceClaims

+ +## Retrieves all resourceClaims. + +`GET /v2/resourceClaims` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Resource Claim Id| +|name|query|string|false|Resource Claim Name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + {} + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[resourceClaimModel](#schemaresourceclaimmodel)]|false|none|none| +|» ResourceClaimModel|[resourceClaimModel](#schemaresourceclaimmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» parentId|integer(int32)¦null|false|none|none| +|»» parentName|string¦null|false|none|none| +|»» children|[[resourceClaimModel](#schemaresourceclaimmodel)]¦null|false|none|Children are collection of SimpleResourceClaimModel| +|»»» ResourceClaimModel|[resourceClaimModel](#schemaresourceclaimmodel)|false|none|none| + + + +## Retrieves a specific resourceClaim based on the identifier. + +`GET /v2/resourceClaims/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[resourceClaimModel](#schemaresourceclaimmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

Vendors

+ +## Retrieves all vendors. + +`GET /v2/vendors` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Vendor/ company id| +|company|query|string|false|Vendor/ company name| +|namespacePrefixes|query|string|false|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|query|string|false|Vendor contact name| +|contactEmailAddress|query|string|false|Vendor contact email id| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[vendorModel](#schemavendormodel)]|false|none|none| +|» Vendor|[vendorModel](#schemavendormodel)|false|none|none| +|»» id|integer(int32)¦null|false|none|none| +|»» company|string¦null|false|none|none| +|»» namespacePrefixes|string¦null|false|none|none| +|»» contactName|string¦null|false|none|none| +|»» contactEmailAddress|string¦null|false|none|none| + + + +## Creates vendor based on the supplied values. + +`POST /v2/vendors` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addVendorRequest](#schemaaddvendorrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific vendor based on the identifier. + +`GET /v2/vendors/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[vendorModel](#schemavendormodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates vendor based on the resource identifier. + +`PUT /v2/vendors/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editVendorRequest](#schemaeditvendorrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing vendor using the resource identifier. + +`DELETE /v2/vendors/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves applications assigned to a specific vendor based on the resource identifier. + +`GET /v2/vendors/{id}/applications` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[applicationModel](#schemaapplicationmodel)]|false|none|none| +|» Application|[applicationModel](#schemaapplicationmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» applicationName|string¦null|false|none|none| +|»» claimSetName|string¦null|false|none|none| +|»» educationOrganizationIds|[integer]¦null|false|none|none| +|»» vendorId|integer(int32)¦null|false|none|none| +|»» profileIds|[integer]¦null|false|none|none| +|»» odsInstanceIds|[integer]¦null|false|none|none| + + + +

Profiles

+ +## Retrieves all profiles. + +`GET /v2/profiles` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Profile id| +|name|query|string|false|Profile name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[profileModel](#schemaprofilemodel)]|false|none|none| +|» Profile|[profileModel](#schemaprofilemodel)|false|none|none| +|»» id|integer(int32)¦null|false|none|none| +|»» name|string¦null|false|none|none| + + + +## Creates profile based on the supplied values. + +`POST /v2/profiles` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addProfileRequest](#schemaaddprofilerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific profile based on the identifier. + +`GET /v2/profiles/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "definition": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[profileDetailsModel](#schemaprofiledetailsmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates profile based on the resource identifier. + +`PUT /v2/profiles/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editProfileRequest](#schemaeditprofilerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing profile using the resource identifier. + +`DELETE /v2/profiles/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

OdsInstances

+ +## Retrieves all odsInstances. + +`GET /v2/odsInstances` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|List of ODS instance id| +|name|query|string|false|Ods Instance name| +|instanceType|query|string|false|none| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "instanceType": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[odsInstanceModel](#schemaodsinstancemodel)]|false|none|none| +|» OdsInstance|[odsInstanceModel](#schemaodsinstancemodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» instanceType|string¦null|false|none|none| + + + +## Creates odsInstance based on the supplied values. + +`POST /v2/odsInstances` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addOdsInstanceRequest](#schemaaddodsinstancerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstance based on the identifier. + +`GET /v2/odsInstances/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string", + "odsInstanceContexts": [ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } + ], + "odsInstanceDerivatives": [ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[odsInstanceDetailModel](#schemaodsinstancedetailmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstance based on the resource identifier. + +`PUT /v2/odsInstances/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editOdsInstanceRequest](#schemaeditodsinstancerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstance using the resource identifier. + +`DELETE /v2/odsInstances/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves applications assigned to a specific ODS instance based on the resource identifier. + +`GET /v2/odsInstances/{id}/applications` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[applicationModel](#schemaapplicationmodel)]|false|none|none| +|» Application|[applicationModel](#schemaapplicationmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» applicationName|string¦null|false|none|none| +|»» claimSetName|string¦null|false|none|none| +|»» educationOrganizationIds|[integer]¦null|false|none|none| +|»» vendorId|integer(int32)¦null|false|none|none| +|»» profileIds|[integer]¦null|false|none|none| +|»» odsInstanceIds|[integer]¦null|false|none|none| + + + +

OdsInstanceDerivatives

+ +## Retrieves all odsInstanceDerivatives. + +`GET /v2/odsInstanceDerivatives` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[odsInstanceDerivativeModel](#schemaodsinstancederivativemodel)]|false|none|none| +|» OdsInstanceDerivative|[odsInstanceDerivativeModel](#schemaodsinstancederivativemodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» odsInstanceId|integer(int32)¦null|false|none|none| +|»» derivativeType|string¦null|false|none|none| + + + +## Creates odsInstanceDerivative based on the supplied values. + +`POST /v2/odsInstanceDerivatives` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addOdsInstanceDerivativeRequest](#schemaaddodsinstancederivativerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstanceDerivative based on the identifier. + +`GET /v2/odsInstanceDerivatives/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[odsInstanceDerivativeModel](#schemaodsinstancederivativemodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstanceDerivative based on the resource identifier. + +`PUT /v2/odsInstanceDerivatives/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editOdsInstanceDerivativeRequest](#schemaeditodsinstancederivativerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstanceDerivative using the resource identifier. + +`DELETE /v2/odsInstanceDerivatives/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

OdsInstanceContexts

+ +## Retrieves all odsInstanceContexts. + +`GET /v2/odsInstanceContexts` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[odsInstanceContextModel](#schemaodsinstancecontextmodel)]|false|none|none| +|» OdsInstanceContext|[odsInstanceContextModel](#schemaodsinstancecontextmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» odsInstanceId|integer(int32)|false|none|none| +|»» contextKey|string¦null|false|none|none| +|»» contextValue|string¦null|false|none|none| + + + +## Creates odsInstanceContext based on the supplied values. + +`POST /v2/odsInstanceContexts` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addOdsInstanceContextRequest](#schemaaddodsinstancecontextrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstanceContext based on the identifier. + +`GET /v2/odsInstanceContexts/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[odsInstanceContextModel](#schemaodsinstancecontextmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstanceContext based on the resource identifier. + +`PUT /v2/odsInstanceContexts/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editOdsInstanceContextRequest](#schemaeditodsinstancecontextrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstanceContext using the resource identifier. + +`DELETE /v2/odsInstanceContexts/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

ClaimSets

+ +## Exports a specific claimset by id + +`GET /v2/claimSets/{id}/export` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[claimSetDetailsModel](#schemaclaimsetdetailsmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves all claimSets. + +`GET /v2/claimSets` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Claim set id| +|name|query|string|false|Claim set name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[claimSetModel](#schemaclaimsetmodel)]|false|none|none| +|» ClaimSet|[claimSetModel](#schemaclaimsetmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» _isSystemReserved|boolean|false|read-only|none| +|»» _applications|[[simpleApplicationModel](#schemasimpleapplicationmodel)]¦null|false|read-only|none| +|»»» Application|[simpleApplicationModel](#schemasimpleapplicationmodel)|false|none|none| +|»»»» applicationName|string¦null|false|none|none| + + + +## Creates claimSet based on the supplied values. + +`POST /v2/claimSets` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "name": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addClaimSetRequest](#schemaaddclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific claimSet based on the identifier. + +`GET /v2/claimSets/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[claimSetDetailsModel](#schemaclaimsetdetailsmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates claimSet based on the resource identifier. + +`PUT /v2/claimSets/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "name": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editClaimSetRequest](#schemaeditclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing claimSet using the resource identifier. + +`DELETE /v2/claimSets/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Copies the existing claimset and create a new one. + +`POST /v2/claimSets/copy` + +> Body parameter + +```json +{ + "originalId": 0, + "name": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[copyClaimSetRequest](#schemacopyclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Imports a new claimset + +`POST /v2/claimSets/import` + +> Body parameter + +```json +{ + "name": "string", + "resourceClaims": [ + { + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[importClaimSetRequest](#schemaimportclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Overrides the default authorization strategies on provided resource claim for a specific action. + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/overrideAuthorizationStrategy` + +Override the default authorization strategies on provided resource claim for a specific action. + +ex: actionName = read, authorizationStrategies= [ "Ownershipbased" ] + +> Body parameter + +```json +{ + "actionName": "string", + "authorizationStrategies": [ + "string" + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| +|body|body|[overrideAuthStategyOnClaimSetRequest](#schemaoverrideauthstategyonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Resets to default authorization strategies on provided resource claim. + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/resetAuthorizationStrategies` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Adds ResourceClaimAction association to a claim set. + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions` + +Add resourceClaimAction association to claim set. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges. +resouceclaimId is required fields. + +> Body parameter + +```json +{ + "resourceClaimId": 0, + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|body|body|[addResourceClaimOnClaimSetRequest](#schemaaddresourceclaimonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates the ResourceClaimActions to a specific resource claim on a claimset. + +`PUT /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}` + +Updates the resourceClaimActions to a specific resource claim on a claimset. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges. + +> Body parameter + +```json +{ + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| +|body|body|[editResourceClaimOnClaimSetRequest](#schemaeditresourceclaimonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes a resource claims association from a claimset + +`DELETE /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

AuthorizationStrategies

+ +## Retrieves all authorizationStrategies. + +`GET /v2/authorizationStrategies` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "displayName": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[authorizationStrategyModel](#schemaauthorizationstrategymodel)]|false|none|none| +|» AuthorizationStrategy|[authorizationStrategyModel](#schemaauthorizationstrategymodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» displayName|string¦null|false|none|none| + + + +

Applications

+ +## Retrieves all applications. + +`GET /v2/applications` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Application id| +|applicationName|query|string|false|Application name| +|claimsetName|query|string|false|Claim set name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[applicationModel](#schemaapplicationmodel)]|false|none|none| +|» Application|[applicationModel](#schemaapplicationmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» applicationName|string¦null|false|none|none| +|»» claimSetName|string¦null|false|none|none| +|»» educationOrganizationIds|[integer]¦null|false|none|none| +|»» vendorId|integer(int32)¦null|false|none|none| +|»» profileIds|[integer]¦null|false|none|none| +|»» odsInstanceIds|[integer]¦null|false|none|none| + + + +## Creates application based on the supplied values. + +`POST /v2/applications` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addApplicationRequest](#schemaaddapplicationrequest)|true|none| + +> Example responses + +> 201 Response + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|[applicationResult](#schemaapplicationresult)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific application based on the identifier. + +`GET /v2/applications/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[applicationModel](#schemaapplicationmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates application based on the resource identifier. + +`PUT /v2/applications/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editApplicationRequest](#schemaeditapplicationrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing application using the resource identifier. + +`DELETE /v2/applications/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Reset application credentials. Returns new key and secret. + +`PUT /v2/applications/{id}/reset-credential` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[applicationResult](#schemaapplicationresult)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

Actions

+ +## Retrieves all actions. + +`GET /v2/actions` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Action id| +|name|query|string|false|Action name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "uri": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[actionModel](#schemaactionmodel)]|false|none|none| +|» Action|[actionModel](#schemaactionmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» uri|string¦null|false|none|none| + + + +

Information

+ +## Retrieve API informational metadata + +`GET /` + +> Example responses + +> 200 Response + +```json +{ + "version": "string", + "build": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[informationResult](#schemainformationresult)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|[informationResult](#schemainformationresult)| + + + +

Connect

+ +## Registers new client + +`POST /connect/register` + +Registers new client + +> Body parameter + +```yaml +ClientId: string +ClientSecret: string +DisplayName: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» ClientId|body|string|false|Client id| +|» ClientSecret|body|string|false|Client secret| +|» DisplayName|body|string|false|Client display name| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Application registered successfully.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves bearer token + +`POST /connect/token` + +To authenticate Swagger requests, execute using "Authorize" above, not "Try It Out" here. + +> Body parameter + +```yaml +client_id: string +client_secret: string +grant_type: string +scope: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» client_id|body|string|false|none| +|» client_secret|body|string|false|none| +|» grant_type|body|string|false|none| +|» scope|body|string|false|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Sign-in successful.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +# Schemas + +

actionModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "uri": "string" +} + +``` + +Action + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|uri|string¦null|false|none|none| + +

addApplicationRequest

+ + + + + + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +AddApplicationRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string|false|none|Application name| +|vendorId|integer(int32)|false|none|Vendor/ company id| +|claimSetName|string|false|none|Claim set name| +|profileIds|[integer]¦null|false|none|Profile id| +|educationOrganizationIds|[integer]|false|none|Education organization ids| +|odsInstanceIds|[integer]|false|none|List of ODS instance id| + +

addClaimSetRequest

+ + + + + + +```json +{ + "name": "string" +} + +``` + +AddClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Claim set name| + +

addOdsInstanceContextRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +AddOdsInstanceContextRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|false|none|ODS instance context ODS instance id.| +|contextKey|string|false|none|context key.| +|contextValue|string|false|none|context value.| + +

addOdsInstanceDerivativeRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} + +``` + +AddOdsInstanceDerivativeRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|false|none|ODS instance derivative ODS instance id.| +|derivativeType|string|false|none|derivative type.| +|connectionString|string|false|none|connection string.| + +

addOdsInstanceRequest

+ + + + + + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} + +``` + +AddOdsInstanceRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Ods Instance name| +|instanceType|string¦null|false|none|Ods Instance type| +|connectionString|string|false|none|Ods Instance connection string| + +

addProfileRequest

+ + + + + + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + +``` + +AddProfileRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Profile name| +|definition|string|false|none|Profile definition| + +

addResourceClaimOnClaimSetRequest

+ + + + + + +```json +{ + "resourceClaimId": 0, + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} + +``` + +AddResourceClaimActionsOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|resourceClaimId|integer(int32)|false|none|ResourceClaim id| +|resourceClaimActions|[[resourceClaimAction](#schemaresourceclaimaction)]|false|none|none| + +

addVendorRequest

+ + + + + + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +AddVendorRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|company|string|false|none|Vendor/ company name| +|namespacePrefixes|string|false|none|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|string|false|none|Vendor contact name| +|contactEmailAddress|string|false|none|Vendor contact email id| + +

adminApiError

+ + + + + + +```json +{ + "title": "string", + "errors": [ + "string" + ] +} + +``` + +AdminApiError + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|title|string¦null|false|read-only|none| +|errors|[string]¦null|false|read-only|none| + +

applicationModel

+ + + + + + +```json +{ + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +Application + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|applicationName|string¦null|false|none|none| +|claimSetName|string¦null|false|none|none| +|educationOrganizationIds|[integer]¦null|false|none|none| +|vendorId|integer(int32)¦null|false|none|none| +|profileIds|[integer]¦null|false|none|none| +|odsInstanceIds|[integer]¦null|false|none|none| + +

applicationResult

+ + + + + + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} + +``` + +ApplicationKeySecret + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|key|string¦null|false|none|none| +|secret|string¦null|false|none|none| + +

authorizationStrategy

+ + + + + + +```json +{ + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true +} + +``` + +ResourceClaimAuthorizationStrategy + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|authStrategyId|integer(int32)|false|none|none| +|authStrategyName|string¦null|false|none|none| +|isInheritedFromParent|boolean|false|none|none| + +

authorizationStrategyModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "displayName": "string" +} + +``` + +AuthorizationStrategy + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|displayName|string¦null|false|none|none| + +

claimSetDetailsModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} + +``` + +ClaimSetWithResources + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|_isSystemReserved|boolean|false|read-only|none| +|_applications|[[simpleApplicationModel](#schemasimpleapplicationmodel)]¦null|false|read-only|none| +|resourceClaims|[[claimSetResourceClaimModel](#schemaclaimsetresourceclaimmodel)]¦null|false|none|none| + +

claimSetModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ] +} + +``` + +ClaimSet + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|_isSystemReserved|boolean|false|read-only|none| +|_applications|[[simpleApplicationModel](#schemasimpleapplicationmodel)]¦null|false|read-only|none| + +

claimSetResourceClaimActionAuthStrategies

+ + + + + + +```json +{ + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] +} + +``` + +ClaimSetResourceClaimActionAuthorizationStrategies + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|actionId|integer(int32)¦null|false|none|none| +|actionName|string¦null|false|none|none| +|authorizationStrategies|[[authorizationStrategy](#schemaauthorizationstrategy)]¦null|false|none|none| + +

claimSetResourceClaimModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [] + } + ] +} + +``` + +ClaimSetResourceClaim + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|read-only|none| +|name|string¦null|false|none|none| +|actions|[[resourceClaimAction](#schemaresourceclaimaction)]¦null|false|none|none| +|_defaultAuthorizationStrategiesForCRUD|[[claimSetResourceClaimActionAuthStrategies](#schemaclaimsetresourceclaimactionauthstrategies)]¦null|false|read-only|none| +|authorizationStrategyOverridesForCRUD|[[claimSetResourceClaimActionAuthStrategies](#schemaclaimsetresourceclaimactionauthstrategies)]¦null|false|none|none| +|children|[[claimSetResourceClaimModel](#schemaclaimsetresourceclaimmodel)]¦null|false|none|Children are collection of ResourceClaim| + +

copyClaimSetRequest

+ + + + + + +```json +{ + "originalId": 0, + "name": "string" +} + +``` + +CopyClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|originalId|integer(int32)|false|none|ClaimSet id to copy| +|name|string|false|none|New claimset name| + +

editApplicationRequest

+ + + + + + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +EditApplicationRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string|false|none|Application name| +|vendorId|integer(int32)|false|none|Vendor/ company id| +|claimSetName|string|false|none|Claim set name| +|profileIds|[integer]¦null|false|none|Profile id| +|educationOrganizationIds|[integer]|false|none|Education organization ids| +|odsInstanceIds|[integer]|false|none|List of ODS instance id| + +

editClaimSetRequest

+ + + + + + +```json +{ + "name": "string" +} + +``` + +EditClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Claim set name| + +

editOdsInstanceContextRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +EditOdsInstanceContextRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|false|none|ODS instance context ODS instance id.| +|contextKey|string|false|none|context key.| +|contextValue|string|false|none|context value.| + +

editOdsInstanceDerivativeRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} + +``` + +EditOdsInstanceDerivativeRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|false|none|ODS instance derivative ODS instance id.| +|derivativeType|string|false|none|derivative type.| +|connectionString|string|false|none|connection string.| + +

editOdsInstanceRequest

+ + + + + + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} + +``` + +EditOdsInstanceRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Ods Instance name| +|instanceType|string¦null|false|none|Ods Instance type| +|connectionString|string¦null|false|none|Ods Instance connection string| + +

editProfileRequest

+ + + + + + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + +``` + +EditProfileRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Profile name| +|definition|string|false|none|Profile definition| + +

editResourceClaimOnClaimSetRequest

+ + + + + + +```json +{ + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} + +``` + +EditResourceClaimActionsOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|resourceClaimActions|[[resourceClaimAction](#schemaresourceclaimaction)]|false|none|none| + +

editVendorRequest

+ + + + + + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +EditVendorRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|company|string|false|none|Vendor/ company name| +|namespacePrefixes|string|false|none|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|string|false|none|Vendor contact name| +|contactEmailAddress|string|false|none|Vendor contact email id| + +

importClaimSetRequest

+ + + + + + +```json +{ + "name": "string", + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} + +``` + +ImportClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Claim set name| +|resourceClaims|[[claimSetResourceClaimModel](#schemaclaimsetresourceclaimmodel)]|false|none|Resource Claims| + +

informationResult

+ + + + + + +```json +{ + "version": "string", + "build": "string" +} + +``` + +Information + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|version|string|false|none|Application version| +|build|string|false|none|Build / release version| + +

odsInstanceContextModel

+ + + + + + +```json +{ + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +OdsInstanceContext + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|odsInstanceId|integer(int32)|false|none|none| +|contextKey|string¦null|false|none|none| +|contextValue|string¦null|false|none|none| + +

odsInstanceDerivativeModel

+ + + + + + +```json +{ + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" +} + +``` + +OdsInstanceDerivative + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|odsInstanceId|integer(int32)¦null|false|none|none| +|derivativeType|string¦null|false|none|none| + +

odsInstanceDetailModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string", + "odsInstanceContexts": [ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } + ], + "odsInstanceDerivatives": [ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } + ] +} + +``` + +OdsInstanceDetail + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|instanceType|string¦null|false|none|none| +|odsInstanceContexts|[[odsInstanceContextModel](#schemaodsinstancecontextmodel)]¦null|false|none|none| +|odsInstanceDerivatives|[[odsInstanceDerivativeModel](#schemaodsinstancederivativemodel)]¦null|false|none|none| + +

odsInstanceModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string" +} + +``` + +OdsInstance + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|instanceType|string¦null|false|none|none| + +

overrideAuthStategyOnClaimSetRequest

+ + + + + + +```json +{ + "actionName": "string", + "authorizationStrategies": [ + "string" + ] +} + +``` + +OverrideAuthStategyOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|actionName|string¦null|false|none|none| +|authorizationStrategies|[string]|false|none|AuthorizationStrategy Names| + +

profileDetailsModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "definition": "string" +} + +``` + +ProfileDetails + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|false|none|none| +|name|string¦null|false|none|none| +|definition|string¦null|false|none|none| + +

profileModel

+ + + + + + +```json +{ + "id": 0, + "name": "string" +} + +``` + +Profile + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|false|none|none| +|name|string¦null|false|none|none| + +

registerClientRequest

+ + + + + + +```json +{ + "clientId": "string", + "clientSecret": "string", + "displayName": "string" +} + +``` + +RegisterClientRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|clientId|string|false|none|Client id| +|clientSecret|string|false|none|Client secret| +|displayName|string|false|none|Client display name| + +

resourceClaimAction

+ + + + + + +```json +{ + "name": "string", + "enabled": true +} + +``` + +ResourceClaimAction + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string¦null|false|none|none| +|enabled|boolean|false|none|none| + +

resourceClaimModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [] + } + ] +} + +``` + +ResourceClaimModel + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|parentId|integer(int32)¦null|false|none|none| +|parentName|string¦null|false|none|none| +|children|[[resourceClaimModel](#schemaresourceclaimmodel)]¦null|false|none|Children are collection of SimpleResourceClaimModel| + +

simpleApplicationModel

+ + + + + + +```json +{ + "applicationName": "string" +} + +``` + +Application + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string¦null|false|none|none| + +

vendorModel

+ + + + + + +```json +{ + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +Vendor + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|false|none|none| +|company|string¦null|false|none|none| +|namespacePrefixes|string¦null|false|none|none| +|contactName|string¦null|false|none|none| +|contactEmailAddress|string¦null|false|none|none| + diff --git a/docs/api-specifications/markdown/admin-api-2.3.0-pre-summary.md b/docs/api-specifications/markdown/admin-api-2.3.0-pre-summary.md new file mode 100644 index 000000000..b012a164d --- /dev/null +++ b/docs/api-specifications/markdown/admin-api-2.3.0-pre-summary.md @@ -0,0 +1,4552 @@ + + +

Admin API Documentation v2

+ +> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu. + +The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API. + +# Authentication + +- oAuth2 authentication. + + - Flow: clientCredentials + + - Token URL = [https://localhost/connect/token](https://localhost/connect/token) + +|Scope|Scope Description| +|---|---| +|edfi_admin_api/full_access|Full access to the Admin API| +|edfi_admin_api/tenant_access|Access to a specific tenant| +|edfi_admin_api/worker|Worker access to the Admin API| + +

ResourceClaims

+ +## Retrieves all resourceClaims. + +`GET /v2/resourceClaims` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Resource Claim Id| +|name|query|string|false|Resource Claim Name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + {} + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[resourceClaimModel](#schemaresourceclaimmodel)]|false|none|none| +|» ResourceClaimModel|[resourceClaimModel](#schemaresourceclaimmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» parentId|integer(int32)¦null|false|none|none| +|»» parentName|string¦null|false|none|none| +|»» children|[[resourceClaimModel](#schemaresourceclaimmodel)]¦null|false|none|Children are collection of SimpleResourceClaimModel| +|»»» ResourceClaimModel|[resourceClaimModel](#schemaresourceclaimmodel)|false|none|none| + + + +## Retrieves a specific resourceClaim based on the identifier. + +`GET /v2/resourceClaims/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[resourceClaimModel](#schemaresourceclaimmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

ResourceClaimActions

+ +## Retrieves all resourceClaimActions. + +`GET /v2/resourceClaimActions` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|resourceName|query|string|false|none| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "resourceClaimId": 0, + "resourceName": "string", + "claimName": "string", + "actions": [ + { + "name": "string" + } + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[resourceClaimActionModel](#schemaresourceclaimactionmodel)]|false|none|none| +|» resourceClaimId|integer(int32)|false|none|none| +|» resourceName|string¦null|false|none|none| +|» claimName|string¦null|false|none|none| +|» actions|[[actionForResourceClaimModel](#schemaactionforresourceclaimmodel)]¦null|false|none|none| +|»» name|string¦null|false|none|none| + + + +

ResourceClaimActionAuthStrategies

+ +## Retrieves all resourceClaimActionAuthStrategies. + +`GET /v2/resourceClaimActionAuthStrategies` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|resourceName|query|string|false|none| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "resourceClaimId": 0, + "resourceName": "string", + "claimName": "string", + "authorizationStrategiesForActions": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string" + } + ] + } + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[resourceClaimActionAuthStrategyModel](#schemaresourceclaimactionauthstrategymodel)]|false|none|none| +|» resourceClaimId|integer(int32)|false|none|none| +|» resourceName|string¦null|false|none|none| +|» claimName|string¦null|false|none|none| +|» authorizationStrategiesForActions|[[actionWithAuthorizationStrategy](#schemaactionwithauthorizationstrategy)]¦null|false|none|none| +|»» actionId|integer(int32)|false|none|none| +|»» actionName|string¦null|false|none|none| +|»» authorizationStrategies|[[authorizationStrategyModelForAction](#schemaauthorizationstrategymodelforaction)]¦null|false|none|none| +|»»» authStrategyId|integer(int32)|false|none|none| +|»»» authStrategyName|string¦null|false|none|none| + + + +

Vendors

+ +## Retrieves all vendors. + +`GET /v2/vendors` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Vendor/ company id| +|company|query|string|false|Vendor/ company name| +|namespacePrefixes|query|string|false|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|query|string|false|Vendor contact name| +|contactEmailAddress|query|string|false|Vendor contact email id| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[vendorModel](#schemavendormodel)]|false|none|none| +|» Vendor|[vendorModel](#schemavendormodel)|false|none|none| +|»» id|integer(int32)¦null|false|none|none| +|»» company|string¦null|false|none|none| +|»» namespacePrefixes|string¦null|false|none|none| +|»» contactName|string¦null|false|none|none| +|»» contactEmailAddress|string¦null|false|none|none| + + + +## Creates vendor based on the supplied values. + +`POST /v2/vendors` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addVendorRequest](#schemaaddvendorrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific vendor based on the identifier. + +`GET /v2/vendors/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[vendorModel](#schemavendormodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates vendor based on the resource identifier. + +`PUT /v2/vendors/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editVendorRequest](#schemaeditvendorrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing vendor using the resource identifier. + +`DELETE /v2/vendors/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves applications assigned to a specific vendor based on the resource identifier. + +`GET /v2/vendors/{id}/applications` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ], + "enabled": true + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[applicationModel](#schemaapplicationmodel)]|false|none|none| +|» Application|[applicationModel](#schemaapplicationmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» applicationName|string¦null|false|none|none| +|»» claimSetName|string¦null|false|none|none| +|»» educationOrganizationIds|[integer]¦null|false|none|none| +|»» vendorId|integer(int32)¦null|false|none|none| +|»» profileIds|[integer]¦null|false|none|none| +|»» odsInstanceIds|[integer]¦null|false|none|none| +|»» enabled|boolean|false|none|none| + + + +

Profiles

+ +## Retrieves all profiles. + +`GET /v2/profiles` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Profile id| +|name|query|string|false|Profile name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[profileModel](#schemaprofilemodel)]|false|none|none| +|» Profile|[profileModel](#schemaprofilemodel)|false|none|none| +|»» id|integer(int32)¦null|false|none|none| +|»» name|string¦null|false|none|none| + + + +## Creates profile based on the supplied values. + +`POST /v2/profiles` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +"{\n \"name\": \"Test-Profile\",\n \"definition\": \"\"\n}" +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addProfileRequest](#schemaaddprofilerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific profile based on the identifier. + +`GET /v2/profiles/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "definition": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[profileDetailsModel](#schemaprofiledetailsmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates profile based on the resource identifier. + +`PUT /v2/profiles/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +"{\n \"name\": \"Test-Profile\",\n \"definition\": \"\"\n}" +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editProfileRequest](#schemaeditprofilerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing profile using the resource identifier. + +`DELETE /v2/profiles/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

OdsInstances

+ +## Retrieves all odsInstances. + +`GET /v2/odsInstances` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|List of ODS instance id| +|name|query|string|false|Ods Instance name| +|instanceType|query|string|false|none| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "instanceType": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[odsInstanceModel](#schemaodsinstancemodel)]|false|none|none| +|» OdsInstance|[odsInstanceModel](#schemaodsinstancemodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» instanceType|string¦null|false|none|none| + + + +## Creates odsInstance based on the supplied values. + +`POST /v2/odsInstances` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addOdsInstanceRequest](#schemaaddodsinstancerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstance based on the identifier. + +`GET /v2/odsInstances/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string", + "odsInstanceContexts": [ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } + ], + "odsInstanceDerivatives": [ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[odsInstanceDetailModel](#schemaodsinstancedetailmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstance based on the resource identifier. + +`PUT /v2/odsInstances/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editOdsInstanceRequest](#schemaeditodsinstancerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstance using the resource identifier. + +`DELETE /v2/odsInstances/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves applications assigned to a specific ODS instance based on the resource identifier. + +`GET /v2/odsInstances/{id}/applications` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ], + "enabled": true + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[applicationModel](#schemaapplicationmodel)]|false|none|none| +|» Application|[applicationModel](#schemaapplicationmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» applicationName|string¦null|false|none|none| +|»» claimSetName|string¦null|false|none|none| +|»» educationOrganizationIds|[integer]¦null|false|none|none| +|»» vendorId|integer(int32)¦null|false|none|none| +|»» profileIds|[integer]¦null|false|none|none| +|»» odsInstanceIds|[integer]¦null|false|none|none| +|»» enabled|boolean|false|none|none| + + + +

OdsInstanceDerivatives

+ +## Retrieves all odsInstanceDerivatives. + +`GET /v2/odsInstanceDerivatives` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[odsInstanceDerivativeModel](#schemaodsinstancederivativemodel)]|false|none|none| +|» OdsInstanceDerivative|[odsInstanceDerivativeModel](#schemaodsinstancederivativemodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» odsInstanceId|integer(int32)¦null|false|none|none| +|»» derivativeType|string¦null|false|none|none| + + + +## Creates odsInstanceDerivative based on the supplied values. + +`POST /v2/odsInstanceDerivatives` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addOdsInstanceDerivativeRequest](#schemaaddodsinstancederivativerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstanceDerivative based on the identifier. + +`GET /v2/odsInstanceDerivatives/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[odsInstanceDerivativeModel](#schemaodsinstancederivativemodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstanceDerivative based on the resource identifier. + +`PUT /v2/odsInstanceDerivatives/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editOdsInstanceDerivativeRequest](#schemaeditodsinstancederivativerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstanceDerivative using the resource identifier. + +`DELETE /v2/odsInstanceDerivatives/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

OdsInstanceContexts

+ +## Retrieves all odsInstanceContexts. + +`GET /v2/odsInstanceContexts` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[odsInstanceContextModel](#schemaodsinstancecontextmodel)]|false|none|none| +|» OdsInstanceContext|[odsInstanceContextModel](#schemaodsinstancecontextmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» odsInstanceId|integer(int32)|false|none|none| +|»» contextKey|string¦null|false|none|none| +|»» contextValue|string¦null|false|none|none| + + + +## Creates odsInstanceContext based on the supplied values. + +`POST /v2/odsInstanceContexts` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addOdsInstanceContextRequest](#schemaaddodsinstancecontextrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstanceContext based on the identifier. + +`GET /v2/odsInstanceContexts/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[odsInstanceContextModel](#schemaodsinstancecontextmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstanceContext based on the resource identifier. + +`PUT /v2/odsInstanceContexts/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editOdsInstanceContextRequest](#schemaeditodsinstancecontextrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstanceContext using the resource identifier. + +`DELETE /v2/odsInstanceContexts/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

ClaimSets

+ +## Exports a specific claimset by id + +`GET /v2/claimSets/{id}/export` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[claimSetDetailsModel](#schemaclaimsetdetailsmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves all claimSets. + +`GET /v2/claimSets` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Claim set id| +|name|query|string|false|Claim set name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[claimSetModel](#schemaclaimsetmodel)]|false|none|none| +|» ClaimSet|[claimSetModel](#schemaclaimsetmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» _isSystemReserved|boolean|false|read-only|none| +|»» _applications|[[simpleApplicationModel](#schemasimpleapplicationmodel)]¦null|false|read-only|none| +|»»» Application|[simpleApplicationModel](#schemasimpleapplicationmodel)|false|none|none| +|»»»» applicationName|string¦null|false|none|none| + + + +## Creates claimSet based on the supplied values. + +`POST /v2/claimSets` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "name": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addClaimSetRequest](#schemaaddclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific claimSet based on the identifier. + +`GET /v2/claimSets/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[claimSetDetailsModel](#schemaclaimsetdetailsmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates claimSet based on the resource identifier. + +`PUT /v2/claimSets/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "name": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editClaimSetRequest](#schemaeditclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing claimSet using the resource identifier. + +`DELETE /v2/claimSets/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Copies the existing claimset and create a new one. + +`POST /v2/claimSets/copy` + +> Body parameter + +```json +{ + "originalId": 0, + "name": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[copyClaimSetRequest](#schemacopyclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Imports a new claimset + +`POST /v2/claimSets/import` + +> Body parameter + +```json +{ + "name": "string", + "resourceClaims": [ + { + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[importClaimSetRequest](#schemaimportclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Overrides the default authorization strategies on provided resource claim for a specific action. + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/overrideAuthorizationStrategy` + +Override the default authorization strategies on provided resource claim for a specific action. + +ex: actionName = read, authorizationStrategies= [ "Ownershipbased" ] + +> Body parameter + +```json +{ + "actionName": "string", + "authorizationStrategies": [ + "string" + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| +|body|body|[overrideAuthStategyOnClaimSetRequest](#schemaoverrideauthstategyonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Resets to default authorization strategies on provided resource claim. + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/resetAuthorizationStrategies` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Adds ResourceClaimAction association to a claim set. + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions` + +Add resourceClaimAction association to claim set. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges. +resouceclaimId is required fields. + +> Body parameter + +```json +{ + "resourceClaimId": 0, + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|body|body|[addResourceClaimOnClaimSetRequest](#schemaaddresourceclaimonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates the ResourceClaimActions to a specific resource claim on a claimset. + +`PUT /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}` + +Updates the resourceClaimActions to a specific resource claim on a claimset. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges. + +> Body parameter + +```json +{ + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| +|body|body|[editResourceClaimOnClaimSetRequest](#schemaeditresourceclaimonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes a resource claims association from a claimset + +`DELETE /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

AuthorizationStrategies

+ +## Retrieves all authorizationStrategies. + +`GET /v2/authorizationStrategies` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "displayName": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[authorizationStrategyModel](#schemaauthorizationstrategymodel)]|false|none|none| +|» AuthorizationStrategy|[authorizationStrategyModel](#schemaauthorizationstrategymodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» displayName|string¦null|false|none|none| + + + +

Applications

+ +## Retrieves all applications. + +`GET /v2/applications` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Application id| +|applicationName|query|string|false|Application name| +|claimsetName|query|string|false|Claim set name| +|ids|query|string|false|none| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ], + "enabled": true + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[applicationModel](#schemaapplicationmodel)]|false|none|none| +|» Application|[applicationModel](#schemaapplicationmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» applicationName|string¦null|false|none|none| +|»» claimSetName|string¦null|false|none|none| +|»» educationOrganizationIds|[integer]¦null|false|none|none| +|»» vendorId|integer(int32)¦null|false|none|none| +|»» profileIds|[integer]¦null|false|none|none| +|»» odsInstanceIds|[integer]¦null|false|none|none| +|»» enabled|boolean|false|none|none| + + + +## Creates application based on the supplied values. + +`POST /v2/applications` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ], + "enabled": true +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addApplicationRequest](#schemaaddapplicationrequest)|true|none| + +> Example responses + +> 201 Response + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|[applicationResult](#schemaapplicationresult)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific application based on the identifier. + +`GET /v2/applications/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ], + "enabled": true +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[applicationModel](#schemaapplicationmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates application based on the resource identifier. + +`PUT /v2/applications/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ], + "enabled": true +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editApplicationRequest](#schemaeditapplicationrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing application using the resource identifier. + +`DELETE /v2/applications/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Reset application credentials. Returns new key and secret. + +`PUT /v2/applications/{id}/reset-credential` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[applicationResult](#schemaapplicationresult)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

Apiclients

+ +## Retrieves all apiclients. + +`GET /v2/apiclients` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|applicationid|query|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "key": "string", + "name": "string", + "isApproved": true, + "useSandbox": true, + "sandboxType": 0, + "applicationId": 0, + "keyStatus": "string", + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[apiClientModel](#schemaapiclientmodel)]|false|none|none| +|» ApiClient|[apiClientModel](#schemaapiclientmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» key|string¦null|false|none|none| +|»» name|string¦null|false|none|none| +|»» isApproved|boolean|false|none|none| +|»» useSandbox|boolean|false|none|none| +|»» sandboxType|integer(int32)|false|none|none| +|»» applicationId|integer(int32)|false|none|none| +|»» keyStatus|string¦null|false|none|none| +|»» educationOrganizationIds|[integer]¦null|false|none|none| +|»» odsInstanceIds|[integer]¦null|false|none|none| + + + +## Creates apiclient based on the supplied values. + +`POST /v2/apiclients` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "name": "string", + "isApproved": true, + "applicationId": 0, + "odsInstanceIds": [ + 0 + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addApiClientRequest](#schemaaddapiclientrequest)|true|none| + +> Example responses + +> 201 Response + +```json +{ + "id": 0, + "name": "string", + "key": "string", + "secret": "string", + "applicationId": 0 +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|[apiClientResult](#schemaapiclientresult)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific apiclient based on the identifier. + +`GET /v2/apiclients/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "key": "string", + "name": "string", + "isApproved": true, + "useSandbox": true, + "sandboxType": 0, + "applicationId": 0, + "keyStatus": "string", + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[apiClientModel](#schemaapiclientmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates apiclient based on the resource identifier. + +`PUT /v2/apiclients/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "name": "string", + "isApproved": true, + "applicationId": 0, + "odsInstanceIds": [ + 0 + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editApiClientRequest](#schemaeditapiclientrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing apiclient using the resource identifier. + +`DELETE /v2/apiclients/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Reset apiclient credentials. Returns new key and secret. + +`PUT /v2/apiclients/{id}/reset-credential` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "key": "string", + "secret": "string", + "applicationId": 0 +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[apiClientResult](#schemaapiclientresult)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

Actions

+ +## Retrieves all actions. + +`GET /v2/actions` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Action id| +|name|query|string|false|Action name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "uri": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[actionModel](#schemaactionmodel)]|false|none|none| +|» Action|[actionModel](#schemaactionmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» uri|string¦null|false|none|none| + + + +

Information

+ +## Retrieve API informational metadata + +`GET /` + +> Example responses + +> 200 Response + +```json +{ + "version": "string", + "build": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[informationResult](#schemainformationresult)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|[informationResult](#schemainformationresult)| + + + +

Connect

+ +## Registers new client + +`POST /connect/register` + +Registers new client + +> Body parameter + +```yaml +ClientId: string +ClientSecret: string +DisplayName: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» ClientId|body|string|false|Client id| +|» ClientSecret|body|string|false|Client secret| +|» DisplayName|body|string|false|Client display name| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Application registered successfully.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves bearer token + +`POST /connect/token` + +To authenticate Swagger requests, execute using "Authorize" above, not "Try It Out" here. + +> Body parameter + +```yaml +client_id: null +client_secret: null +grant_type: null +scope: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» client_id|body|string |false|none| +|» client_secret|body|string |false|none| +|» grant_type|body|string |false|none| +|» scope|body|string|false|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Sign-in successful.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request, such as invalid scope.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +# Schemas + +

actionForResourceClaimModel

+ + + + + + +```json +{ + "name": "string" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string¦null|false|none|none| + +

actionModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "uri": "string" +} + +``` + +Action + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|uri|string¦null|false|none|none| + +

actionWithAuthorizationStrategy

+ + + + + + +```json +{ + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string" + } + ] +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|actionId|integer(int32)|false|none|none| +|actionName|string¦null|false|none|none| +|authorizationStrategies|[[authorizationStrategyModelForAction](#schemaauthorizationstrategymodelforaction)]¦null|false|none|none| + +

addApiClientRequest

+ + + + + + +```json +{ + "name": "string", + "isApproved": true, + "applicationId": 0, + "odsInstanceIds": [ + 0 + ] +} + +``` + +AddApiClientRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Api client name| +|isApproved|boolean|false|none|Is approved| +|applicationId|integer(int32)|false|none|Application id| +|odsInstanceIds|[integer]|false|none|List of ODS instance id| + +

addApplicationRequest

+ + + + + + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ], + "enabled": true +} + +``` + +AddApplicationRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string|false|none|Application name| +|vendorId|integer(int32)|false|none|Vendor/ company id| +|claimSetName|string|false|none|Claim set name| +|profileIds|[integer]¦null|false|none|Profile id| +|educationOrganizationIds|[integer]|false|none|Education organization ids| +|odsInstanceIds|[integer]|false|none|List of ODS instance id| +|enabled|boolean¦null|false|none|Indicates whether the ApiClient's credetials is enabled. Defaults to true if not provided.| + +

addClaimSetRequest

+ + + + + + +```json +{ + "name": "string" +} + +``` + +AddClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Claim set name| + +

addOdsInstanceContextRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +AddOdsInstanceContextRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|false|none|ODS instance context ODS instance id.| +|contextKey|string|false|none|context key.| +|contextValue|string|false|none|context value.| + +

addOdsInstanceDerivativeRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} + +``` + +AddOdsInstanceDerivativeRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|false|none|ODS instance derivative ODS instance id.| +|derivativeType|string|false|none|derivative type.| +|connectionString|string|false|none|connection string.| + +

addOdsInstanceRequest

+ + + + + + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} + +``` + +AddOdsInstanceRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Ods Instance name| +|instanceType|string¦null|false|none|Ods Instance type| +|connectionString|string|false|none|Ods Instance connection string| + +

addProfileRequest

+ + + + + + +```json +"{\n \"name\": \"Test-Profile\",\n \"definition\": \"\"\n}" + +``` + +AddProfileRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Profile name| +|definition|string|false|none|Profile definition| + +

addResourceClaimOnClaimSetRequest

+ + + + + + +```json +{ + "resourceClaimId": 0, + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} + +``` + +AddResourceClaimActionsOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|resourceClaimId|integer(int32)|false|none|ResourceClaim id| +|resourceClaimActions|[[resourceClaimAction](#schemaresourceclaimaction)]|false|none|none| + +

addVendorRequest

+ + + + + + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +AddVendorRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|company|string|false|none|Vendor/ company name| +|namespacePrefixes|string|false|none|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|string|false|none|Vendor contact name| +|contactEmailAddress|string|false|none|Vendor contact email id| + +

adminApiError

+ + + + + + +```json +{} + +``` + +AdminApiError + +### Properties + +*None* + +

apiClientModel

+ + + + + + +```json +{ + "id": 0, + "key": "string", + "name": "string", + "isApproved": true, + "useSandbox": true, + "sandboxType": 0, + "applicationId": 0, + "keyStatus": "string", + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +ApiClient + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|key|string¦null|false|none|none| +|name|string¦null|false|none|none| +|isApproved|boolean|false|none|none| +|useSandbox|boolean|false|none|none| +|sandboxType|integer(int32)|false|none|none| +|applicationId|integer(int32)|false|none|none| +|keyStatus|string¦null|false|none|none| +|educationOrganizationIds|[integer]¦null|false|none|none| +|odsInstanceIds|[integer]¦null|false|none|none| + +

apiClientResult

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "key": "string", + "secret": "string", + "applicationId": 0 +} + +``` + +ApiClient + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|key|string¦null|false|none|none| +|secret|string¦null|false|none|none| +|applicationId|integer(int32)|false|none|none| + +

applicationModel

+ + + + + + +```json +{ + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ], + "enabled": true +} + +``` + +Application + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|applicationName|string¦null|false|none|none| +|claimSetName|string¦null|false|none|none| +|educationOrganizationIds|[integer]¦null|false|none|none| +|vendorId|integer(int32)¦null|false|none|none| +|profileIds|[integer]¦null|false|none|none| +|odsInstanceIds|[integer]¦null|false|none|none| +|enabled|boolean|false|none|none| + +

applicationResult

+ + + + + + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} + +``` + +ApplicationKeySecret + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|key|string¦null|false|none|none| +|secret|string¦null|false|none|none| + +

authorizationStrategy

+ + + + + + +```json +{ + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true +} + +``` + +ResourceClaimAuthorizationStrategy + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|authStrategyId|integer(int32)|false|none|none| +|authStrategyName|string¦null|false|none|none| +|isInheritedFromParent|boolean|false|none|none| + +

authorizationStrategyModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "displayName": "string" +} + +``` + +AuthorizationStrategy + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|displayName|string¦null|false|none|none| + +

authorizationStrategyModelForAction

+ + + + + + +```json +{ + "authStrategyId": 0, + "authStrategyName": "string" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|authStrategyId|integer(int32)|false|none|none| +|authStrategyName|string¦null|false|none|none| + +

claimSetDetailsModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} + +``` + +ClaimSetWithResources + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|_isSystemReserved|boolean|false|read-only|none| +|_applications|[[simpleApplicationModel](#schemasimpleapplicationmodel)]¦null|false|read-only|none| +|resourceClaims|[[claimSetResourceClaimModel](#schemaclaimsetresourceclaimmodel)]¦null|false|none|none| + +

claimSetModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ] +} + +``` + +ClaimSet + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|_isSystemReserved|boolean|false|read-only|none| +|_applications|[[simpleApplicationModel](#schemasimpleapplicationmodel)]¦null|false|read-only|none| + +

claimSetResourceClaimActionAuthStrategies

+ + + + + + +```json +{ + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] +} + +``` + +ClaimSetResourceClaimActionAuthorizationStrategies + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|actionId|integer(int32)¦null|false|none|none| +|actionName|string¦null|false|none|none| +|authorizationStrategies|[[authorizationStrategy](#schemaauthorizationstrategy)]¦null|false|none|none| + +

claimSetResourceClaimModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [] + } + ] +} + +``` + +ClaimSetResourceClaim + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|read-only|none| +|name|string¦null|false|none|none| +|actions|[[resourceClaimAction](#schemaresourceclaimaction)]¦null|false|none|none| +|_defaultAuthorizationStrategiesForCRUD|[[claimSetResourceClaimActionAuthStrategies](#schemaclaimsetresourceclaimactionauthstrategies)]¦null|false|read-only|none| +|authorizationStrategyOverridesForCRUD|[[claimSetResourceClaimActionAuthStrategies](#schemaclaimsetresourceclaimactionauthstrategies)]¦null|false|none|none| +|children|[[claimSetResourceClaimModel](#schemaclaimsetresourceclaimmodel)]¦null|false|none|Children are collection of ResourceClaim| + +

copyClaimSetRequest

+ + + + + + +```json +{ + "originalId": 0, + "name": "string" +} + +``` + +CopyClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|originalId|integer(int32)|false|none|ClaimSet id to copy| +|name|string|false|none|New claimset name| + +

editApiClientRequest

+ + + + + + +```json +{ + "name": "string", + "isApproved": true, + "applicationId": 0, + "odsInstanceIds": [ + 0 + ] +} + +``` + +EditApiClientRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Api client name| +|isApproved|boolean|false|none|Is approved| +|applicationId|integer(int32)|false|none|Application id| +|odsInstanceIds|[integer]|false|none|List of ODS instance id| + +

editApplicationRequest

+ + + + + + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ], + "enabled": true +} + +``` + +EditApplicationRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string|false|none|Application name| +|vendorId|integer(int32)|false|none|Vendor/ company id| +|claimSetName|string|false|none|Claim set name| +|profileIds|[integer]¦null|false|none|Profile id| +|educationOrganizationIds|[integer]|false|none|Education organization ids| +|odsInstanceIds|[integer]|false|none|List of ODS instance id| +|enabled|boolean¦null|false|none|Indicates whether the ApiClient's credetials is enabled. Defaults to true if not provided.| + +

editClaimSetRequest

+ + + + + + +```json +{ + "name": "string" +} + +``` + +EditClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Claim set name| + +

editOdsInstanceContextRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +EditOdsInstanceContextRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|false|none|ODS instance context ODS instance id.| +|contextKey|string|false|none|context key.| +|contextValue|string|false|none|context value.| + +

editOdsInstanceDerivativeRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} + +``` + +EditOdsInstanceDerivativeRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|false|none|ODS instance derivative ODS instance id.| +|derivativeType|string|false|none|derivative type.| +|connectionString|string|false|none|connection string.| + +

editOdsInstanceRequest

+ + + + + + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} + +``` + +EditOdsInstanceRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Ods Instance name| +|instanceType|string¦null|false|none|Ods Instance type| +|connectionString|string¦null|false|none|Ods Instance connection string| + +

editProfileRequest

+ + + + + + +```json +"{\n \"name\": \"Test-Profile\",\n \"definition\": \"\"\n}" + +``` + +EditProfileRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Profile name| +|definition|string|false|none|Profile definition| + +

editResourceClaimOnClaimSetRequest

+ + + + + + +```json +{ + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} + +``` + +EditResourceClaimActionsOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|resourceClaimActions|[[resourceClaimAction](#schemaresourceclaimaction)]|false|none|none| + +

editVendorRequest

+ + + + + + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +EditVendorRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|company|string|false|none|Vendor/ company name| +|namespacePrefixes|string|false|none|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|string|false|none|Vendor contact name| +|contactEmailAddress|string|false|none|Vendor contact email id| + +

importClaimSetRequest

+ + + + + + +```json +{ + "name": "string", + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} + +``` + +ImportClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Claim set name| +|resourceClaims|[[claimSetResourceClaimModel](#schemaclaimsetresourceclaimmodel)]|false|none|Resource Claims| + +

informationResult

+ + + + + + +```json +{ + "version": "string", + "build": "string" +} + +``` + +Information + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|version|string|false|none|Application version| +|build|string|false|none|Build / release version| + +

odsInstanceContextModel

+ + + + + + +```json +{ + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +OdsInstanceContext + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|odsInstanceId|integer(int32)|false|none|none| +|contextKey|string¦null|false|none|none| +|contextValue|string¦null|false|none|none| + +

odsInstanceDerivativeModel

+ + + + + + +```json +{ + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" +} + +``` + +OdsInstanceDerivative + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|odsInstanceId|integer(int32)¦null|false|none|none| +|derivativeType|string¦null|false|none|none| + +

odsInstanceDetailModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string", + "odsInstanceContexts": [ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } + ], + "odsInstanceDerivatives": [ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } + ] +} + +``` + +OdsInstanceDetail + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|instanceType|string¦null|false|none|none| +|odsInstanceContexts|[[odsInstanceContextModel](#schemaodsinstancecontextmodel)]¦null|false|none|none| +|odsInstanceDerivatives|[[odsInstanceDerivativeModel](#schemaodsinstancederivativemodel)]¦null|false|none|none| + +

odsInstanceModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string" +} + +``` + +OdsInstance + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|instanceType|string¦null|false|none|none| + +

overrideAuthStategyOnClaimSetRequest

+ + + + + + +```json +{ + "actionName": "string", + "authorizationStrategies": [ + "string" + ] +} + +``` + +OverrideAuthStategyOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|actionName|string¦null|false|none|none| +|authorizationStrategies|[string]|false|none|AuthorizationStrategy Names| + +

profileDetailsModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "definition": "string" +} + +``` + +ProfileDetails + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|false|none|none| +|name|string¦null|false|none|none| +|definition|string¦null|false|none|none| + +

profileModel

+ + + + + + +```json +{ + "id": 0, + "name": "string" +} + +``` + +Profile + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|false|none|none| +|name|string¦null|false|none|none| + +

registerClientRequest

+ + + + + + +```json +{ + "clientId": "string", + "clientSecret": "string", + "displayName": "string" +} + +``` + +RegisterClientRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|clientId|string|false|none|Client id| +|clientSecret|string|false|none|Client secret| +|displayName|string|false|none|Client display name| + +

resourceClaimAction

+ + + + + + +```json +{ + "name": "string", + "enabled": true +} + +``` + +ResourceClaimAction + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string¦null|false|none|none| +|enabled|boolean|false|none|none| + +

resourceClaimActionAuthStrategyModel

+ + + + + + +```json +{ + "resourceClaimId": 0, + "resourceName": "string", + "claimName": "string", + "authorizationStrategiesForActions": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string" + } + ] + } + ] +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|resourceClaimId|integer(int32)|false|none|none| +|resourceName|string¦null|false|none|none| +|claimName|string¦null|false|none|none| +|authorizationStrategiesForActions|[[actionWithAuthorizationStrategy](#schemaactionwithauthorizationstrategy)]¦null|false|none|none| + +

resourceClaimActionModel

+ + + + + + +```json +{ + "resourceClaimId": 0, + "resourceName": "string", + "claimName": "string", + "actions": [ + { + "name": "string" + } + ] +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|resourceClaimId|integer(int32)|false|none|none| +|resourceName|string¦null|false|none|none| +|claimName|string¦null|false|none|none| +|actions|[[actionForResourceClaimModel](#schemaactionforresourceclaimmodel)]¦null|false|none|none| + +

resourceClaimModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [] + } + ] +} + +``` + +ResourceClaimModel + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|parentId|integer(int32)¦null|false|none|none| +|parentName|string¦null|false|none|none| +|children|[[resourceClaimModel](#schemaresourceclaimmodel)]¦null|false|none|Children are collection of SimpleResourceClaimModel| + +

simpleApplicationModel

+ + + + + + +```json +{ + "applicationName": "string" +} + +``` + +Application + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string¦null|false|none|none| + +

vendorModel

+ + + + + + +```json +{ + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +Vendor + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|false|none|none| +|company|string¦null|false|none|none| +|namespacePrefixes|string¦null|false|none|none| +|contactName|string¦null|false|none|none| +|contactEmailAddress|string¦null|false|none|none| + diff --git a/docs/api-specifications/markdown/admin-api-2.3.0-summary-pre.md b/docs/api-specifications/markdown/admin-api-2.3.0-summary-pre.md new file mode 100644 index 000000000..c4bfb4101 --- /dev/null +++ b/docs/api-specifications/markdown/admin-api-2.3.0-summary-pre.md @@ -0,0 +1,3824 @@ + + +

Admin API Documentation v2

+ +> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu. + +The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API. + +# Authentication + +- oAuth2 authentication. + + - Flow: clientCredentials + + - Token URL = [https://localhost/connect/token](https://localhost/connect/token) + +|Scope|Scope Description| +|---|---| +|edfi_admin_api/full_access|Unrestricted access to all Admin API endpoints| + +

ResourceClaims

+ +## Retrieves all resourceClaims. + +`GET /v2/resourceClaims` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Resource Claim Id| +|name|query|string|false|Resource Claim Name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + {} + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[resourceClaimModel](#schemaresourceclaimmodel)]|false|none|none| +|» ResourceClaimModel|[resourceClaimModel](#schemaresourceclaimmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» parentId|integer(int32)¦null|false|none|none| +|»» parentName|string¦null|false|none|none| +|»» children|[[resourceClaimModel](#schemaresourceclaimmodel)]¦null|false|none|Children are collection of SimpleResourceClaimModel| +|»»» ResourceClaimModel|[resourceClaimModel](#schemaresourceclaimmodel)|false|none|none| + + + +## Retrieves a specific resourceClaim based on the identifier. + +`GET /v2/resourceClaims/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[resourceClaimModel](#schemaresourceclaimmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

Vendors

+ +## Retrieves all vendors. + +`GET /v2/vendors` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Vendor/ company id| +|company|query|string|false|Vendor/ company name| +|namespacePrefixes|query|string|false|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|query|string|false|Vendor contact name| +|contactEmailAddress|query|string|false|Vendor contact email id| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[vendorModel](#schemavendormodel)]|false|none|none| +|» Vendor|[vendorModel](#schemavendormodel)|false|none|none| +|»» id|integer(int32)¦null|false|none|none| +|»» company|string¦null|false|none|none| +|»» namespacePrefixes|string¦null|false|none|none| +|»» contactName|string¦null|false|none|none| +|»» contactEmailAddress|string¦null|false|none|none| + + + +## Creates vendor based on the supplied values. + +`POST /v2/vendors` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addVendorRequest](#schemaaddvendorrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific vendor based on the identifier. + +`GET /v2/vendors/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[vendorModel](#schemavendormodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates vendor based on the resource identifier. + +`PUT /v2/vendors/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editVendorRequest](#schemaeditvendorrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing vendor using the resource identifier. + +`DELETE /v2/vendors/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves applications assigned to a specific vendor based on the resource identifier. + +`GET /v2/vendors/{id}/applications` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[applicationModel](#schemaapplicationmodel)]|false|none|none| +|» Application|[applicationModel](#schemaapplicationmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» applicationName|string¦null|false|none|none| +|»» claimSetName|string¦null|false|none|none| +|»» educationOrganizationIds|[integer]¦null|false|none|none| +|»» vendorId|integer(int32)¦null|false|none|none| +|»» profileIds|[integer]¦null|false|none|none| +|»» odsInstanceIds|[integer]¦null|false|none|none| + + + +

Profiles

+ +## Retrieves all profiles. + +`GET /v2/profiles` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Profile id| +|name|query|string|false|Profile name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[profileModel](#schemaprofilemodel)]|false|none|none| +|» Profile|[profileModel](#schemaprofilemodel)|false|none|none| +|»» id|integer(int32)¦null|false|none|none| +|»» name|string¦null|false|none|none| + + + +## Creates profile based on the supplied values. + +`POST /v2/profiles` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addProfileRequest](#schemaaddprofilerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific profile based on the identifier. + +`GET /v2/profiles/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "definition": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[profileDetailsModel](#schemaprofiledetailsmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates profile based on the resource identifier. + +`PUT /v2/profiles/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editProfileRequest](#schemaeditprofilerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing profile using the resource identifier. + +`DELETE /v2/profiles/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

OdsInstances

+ +## Retrieves all odsInstances. + +`GET /v2/odsInstances` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|List of ODS instance id| +|name|query|string|false|Ods Instance name| +|instanceType|query|string|false|none| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "instanceType": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[odsInstanceModel](#schemaodsinstancemodel)]|false|none|none| +|» OdsInstance|[odsInstanceModel](#schemaodsinstancemodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» instanceType|string¦null|false|none|none| + + + +## Creates odsInstance based on the supplied values. + +`POST /v2/odsInstances` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addOdsInstanceRequest](#schemaaddodsinstancerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstance based on the identifier. + +`GET /v2/odsInstances/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string", + "odsInstanceContexts": [ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } + ], + "odsInstanceDerivatives": [ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[odsInstanceDetailModel](#schemaodsinstancedetailmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstance based on the resource identifier. + +`PUT /v2/odsInstances/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editOdsInstanceRequest](#schemaeditodsinstancerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstance using the resource identifier. + +`DELETE /v2/odsInstances/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves applications assigned to a specific ODS instance based on the resource identifier. + +`GET /v2/odsInstances/{id}/applications` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[applicationModel](#schemaapplicationmodel)]|false|none|none| +|» Application|[applicationModel](#schemaapplicationmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» applicationName|string¦null|false|none|none| +|»» claimSetName|string¦null|false|none|none| +|»» educationOrganizationIds|[integer]¦null|false|none|none| +|»» vendorId|integer(int32)¦null|false|none|none| +|»» profileIds|[integer]¦null|false|none|none| +|»» odsInstanceIds|[integer]¦null|false|none|none| + + + +

OdsInstanceDerivatives

+ +## Retrieves all odsInstanceDerivatives. + +`GET /v2/odsInstanceDerivatives` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[odsInstanceDerivativeModel](#schemaodsinstancederivativemodel)]|false|none|none| +|» OdsInstanceDerivative|[odsInstanceDerivativeModel](#schemaodsinstancederivativemodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» odsInstanceId|integer(int32)¦null|false|none|none| +|»» derivativeType|string¦null|false|none|none| + + + +## Creates odsInstanceDerivative based on the supplied values. + +`POST /v2/odsInstanceDerivatives` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addOdsInstanceDerivativeRequest](#schemaaddodsinstancederivativerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstanceDerivative based on the identifier. + +`GET /v2/odsInstanceDerivatives/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[odsInstanceDerivativeModel](#schemaodsinstancederivativemodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstanceDerivative based on the resource identifier. + +`PUT /v2/odsInstanceDerivatives/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editOdsInstanceDerivativeRequest](#schemaeditodsinstancederivativerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstanceDerivative using the resource identifier. + +`DELETE /v2/odsInstanceDerivatives/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

OdsInstanceContexts

+ +## Retrieves all odsInstanceContexts. + +`GET /v2/odsInstanceContexts` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[odsInstanceContextModel](#schemaodsinstancecontextmodel)]|false|none|none| +|» OdsInstanceContext|[odsInstanceContextModel](#schemaodsinstancecontextmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» odsInstanceId|integer(int32)|false|none|none| +|»» contextKey|string¦null|false|none|none| +|»» contextValue|string¦null|false|none|none| + + + +## Creates odsInstanceContext based on the supplied values. + +`POST /v2/odsInstanceContexts` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addOdsInstanceContextRequest](#schemaaddodsinstancecontextrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstanceContext based on the identifier. + +`GET /v2/odsInstanceContexts/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[odsInstanceContextModel](#schemaodsinstancecontextmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstanceContext based on the resource identifier. + +`PUT /v2/odsInstanceContexts/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editOdsInstanceContextRequest](#schemaeditodsinstancecontextrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstanceContext using the resource identifier. + +`DELETE /v2/odsInstanceContexts/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

ClaimSets

+ +## Exports a specific claimset by id + +`GET /v2/claimSets/{id}/export` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[claimSetDetailsModel](#schemaclaimsetdetailsmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves all claimSets. + +`GET /v2/claimSets` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Claim set id| +|name|query|string|false|Claim set name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[claimSetModel](#schemaclaimsetmodel)]|false|none|none| +|» ClaimSet|[claimSetModel](#schemaclaimsetmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» _isSystemReserved|boolean|false|read-only|none| +|»» _applications|[[simpleApplicationModel](#schemasimpleapplicationmodel)]¦null|false|read-only|none| +|»»» Application|[simpleApplicationModel](#schemasimpleapplicationmodel)|false|none|none| +|»»»» applicationName|string¦null|false|none|none| + + + +## Creates claimSet based on the supplied values. + +`POST /v2/claimSets` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "name": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addClaimSetRequest](#schemaaddclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific claimSet based on the identifier. + +`GET /v2/claimSets/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[claimSetDetailsModel](#schemaclaimsetdetailsmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates claimSet based on the resource identifier. + +`PUT /v2/claimSets/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "name": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editClaimSetRequest](#schemaeditclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing claimSet using the resource identifier. + +`DELETE /v2/claimSets/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Copies the existing claimset and create a new one. + +`POST /v2/claimSets/copy` + +> Body parameter + +```json +{ + "originalId": 0, + "name": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[copyClaimSetRequest](#schemacopyclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Imports a new claimset + +`POST /v2/claimSets/import` + +> Body parameter + +```json +{ + "name": "string", + "resourceClaims": [ + { + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[importClaimSetRequest](#schemaimportclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Overrides the default authorization strategies on provided resource claim for a specific action. + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/overrideAuthorizationStrategy` + +Override the default authorization strategies on provided resource claim for a specific action. + +ex: actionName = read, authorizationStrategies= [ "Ownershipbased" ] + +> Body parameter + +```json +{ + "actionName": "string", + "authorizationStrategies": [ + "string" + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| +|body|body|[overrideAuthStategyOnClaimSetRequest](#schemaoverrideauthstategyonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Resets to default authorization strategies on provided resource claim. + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/resetAuthorizationStrategies` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Adds ResourceClaimAction association to a claim set. + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions` + +Add resourceClaimAction association to claim set. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges. +resouceclaimId is required fields. + +> Body parameter + +```json +{ + "resourceClaimId": 0, + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|body|body|[addResourceClaimOnClaimSetRequest](#schemaaddresourceclaimonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates the ResourceClaimActions to a specific resource claim on a claimset. + +`PUT /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}` + +Updates the resourceClaimActions to a specific resource claim on a claimset. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges. + +> Body parameter + +```json +{ + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| +|body|body|[editResourceClaimOnClaimSetRequest](#schemaeditresourceclaimonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes a resource claims association from a claimset + +`DELETE /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

AuthorizationStrategies

+ +## Retrieves all authorizationStrategies. + +`GET /v2/authorizationStrategies` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "displayName": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[authorizationStrategyModel](#schemaauthorizationstrategymodel)]|false|none|none| +|» AuthorizationStrategy|[authorizationStrategyModel](#schemaauthorizationstrategymodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» displayName|string¦null|false|none|none| + + + +

Applications

+ +## Retrieves all applications. + +`GET /v2/applications` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Application id| +|applicationName|query|string|false|Application name| +|claimsetName|query|string|false|Claim set name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[applicationModel](#schemaapplicationmodel)]|false|none|none| +|» Application|[applicationModel](#schemaapplicationmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» applicationName|string¦null|false|none|none| +|»» claimSetName|string¦null|false|none|none| +|»» educationOrganizationIds|[integer]¦null|false|none|none| +|»» vendorId|integer(int32)¦null|false|none|none| +|»» profileIds|[integer]¦null|false|none|none| +|»» odsInstanceIds|[integer]¦null|false|none|none| + + + +## Creates application based on the supplied values. + +`POST /v2/applications` + +The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used. + +> Body parameter + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[addApplicationRequest](#schemaaddapplicationrequest)|true|none| + +> Example responses + +> 201 Response + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|[applicationResult](#schemaapplicationresult)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific application based on the identifier. + +`GET /v2/applications/{id}` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[applicationModel](#schemaapplicationmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates application based on the resource identifier. + +`PUT /v2/applications/{id}` + +The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior. + +> Body parameter + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[editApplicationRequest](#schemaeditapplicationrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing application using the resource identifier. + +`DELETE /v2/applications/{id}` + +The DELETE operation is used to delete an existing resource by identifier. If the resource doesn't exist, an error will result (the resource will not be found). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Reset application credentials. Returns new key and secret. + +`PUT /v2/applications/{id}/reset-credential` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[applicationResult](#schemaapplicationresult)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

Actions

+ +## Retrieves all actions. + +`GET /v2/actions` + +This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Action id| +|name|query|string|false|Action name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "uri": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[actionModel](#schemaactionmodel)]|false|none|none| +|» Action|[actionModel](#schemaactionmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» uri|string¦null|false|none|none| + + + +

Information

+ +## Retrieve API informational metadata + +`GET /` + +> Example responses + +> 200 Response + +```json +{ + "version": "string", + "build": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[informationResult](#schemainformationresult)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|[informationResult](#schemainformationresult)| + + + +

Connect

+ +## Registers new client + +`POST /connect/register` + +Registers new client + +> Body parameter + +```yaml +ClientId: string +ClientSecret: string +DisplayName: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» ClientId|body|string|false|Client id| +|» ClientSecret|body|string|false|Client secret| +|» DisplayName|body|string|false|Client display name| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Application registered successfully.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves bearer token + +`POST /connect/token` + +To authenticate Swagger requests, execute using "Authorize" above, not "Try It Out" here. + +> Body parameter + +```yaml +client_id: null +client_secret: null +grant_type: null +scope: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» client_id|body|string |false|none| +|» client_secret|body|string |false|none| +|» grant_type|body|string |false|none| +|» scope|body|string|false|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Sign-in successful.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +# Schemas + +

actionModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "uri": "string" +} + +``` + +Action + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|uri|string¦null|false|none|none| + +

addApplicationRequest

+ + + + + + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +AddApplicationRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string|false|none|Application name| +|vendorId|integer(int32)|false|none|Vendor/ company id| +|claimSetName|string|false|none|Claim set name| +|profileIds|[integer]¦null|false|none|Profile id| +|educationOrganizationIds|[integer]|false|none|Education organization ids| +|odsInstanceIds|[integer]|false|none|List of ODS instance id| + +

addClaimSetRequest

+ + + + + + +```json +{ + "name": "string" +} + +``` + +AddClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Claim set name| + +

addOdsInstanceContextRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +AddOdsInstanceContextRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|false|none|ODS instance context ODS instance id.| +|contextKey|string|false|none|context key.| +|contextValue|string|false|none|context value.| + +

addOdsInstanceDerivativeRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} + +``` + +AddOdsInstanceDerivativeRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|false|none|ODS instance derivative ODS instance id.| +|derivativeType|string|false|none|derivative type.| +|connectionString|string|false|none|connection string.| + +

addOdsInstanceRequest

+ + + + + + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} + +``` + +AddOdsInstanceRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Ods Instance name| +|instanceType|string¦null|false|none|Ods Instance type| +|connectionString|string|false|none|Ods Instance connection string| + +

addProfileRequest

+ + + + + + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + +``` + +AddProfileRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Profile name| +|definition|string|false|none|Profile definition| + +

addResourceClaimOnClaimSetRequest

+ + + + + + +```json +{ + "resourceClaimId": 0, + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} + +``` + +AddResourceClaimActionsOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|resourceClaimId|integer(int32)|false|none|ResourceClaim id| +|resourceClaimActions|[[resourceClaimAction](#schemaresourceclaimaction)]|false|none|none| + +

addVendorRequest

+ + + + + + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +AddVendorRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|company|string|false|none|Vendor/ company name| +|namespacePrefixes|string|false|none|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|string|false|none|Vendor contact name| +|contactEmailAddress|string|false|none|Vendor contact email id| + +

adminApiError

+ + + + + + +```json +{ + "title": "string", + "errors": [ + "string" + ] +} + +``` + +AdminApiError + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|title|string¦null|false|read-only|none| +|errors|[string]¦null|false|read-only|none| + +

applicationModel

+ + + + + + +```json +{ + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +Application + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|applicationName|string¦null|false|none|none| +|claimSetName|string¦null|false|none|none| +|educationOrganizationIds|[integer]¦null|false|none|none| +|vendorId|integer(int32)¦null|false|none|none| +|profileIds|[integer]¦null|false|none|none| +|odsInstanceIds|[integer]¦null|false|none|none| + +

applicationResult

+ + + + + + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} + +``` + +ApplicationKeySecret + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|key|string¦null|false|none|none| +|secret|string¦null|false|none|none| + +

authorizationStrategy

+ + + + + + +```json +{ + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true +} + +``` + +ResourceClaimAuthorizationStrategy + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|authStrategyId|integer(int32)|false|none|none| +|authStrategyName|string¦null|false|none|none| +|isInheritedFromParent|boolean|false|none|none| + +

authorizationStrategyModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "displayName": "string" +} + +``` + +AuthorizationStrategy + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|displayName|string¦null|false|none|none| + +

claimSetDetailsModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} + +``` + +ClaimSetWithResources + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|_isSystemReserved|boolean|false|read-only|none| +|_applications|[[simpleApplicationModel](#schemasimpleapplicationmodel)]¦null|false|read-only|none| +|resourceClaims|[[claimSetResourceClaimModel](#schemaclaimsetresourceclaimmodel)]¦null|false|none|none| + +

claimSetModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ] +} + +``` + +ClaimSet + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|_isSystemReserved|boolean|false|read-only|none| +|_applications|[[simpleApplicationModel](#schemasimpleapplicationmodel)]¦null|false|read-only|none| + +

claimSetResourceClaimActionAuthStrategies

+ + + + + + +```json +{ + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] +} + +``` + +ClaimSetResourceClaimActionAuthorizationStrategies + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|actionId|integer(int32)¦null|false|none|none| +|actionName|string¦null|false|none|none| +|authorizationStrategies|[[authorizationStrategy](#schemaauthorizationstrategy)]¦null|false|none|none| + +

claimSetResourceClaimModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [] + } + ] +} + +``` + +ClaimSetResourceClaim + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|read-only|none| +|name|string¦null|false|none|none| +|actions|[[resourceClaimAction](#schemaresourceclaimaction)]¦null|false|none|none| +|_defaultAuthorizationStrategiesForCRUD|[[claimSetResourceClaimActionAuthStrategies](#schemaclaimsetresourceclaimactionauthstrategies)]¦null|false|read-only|none| +|authorizationStrategyOverridesForCRUD|[[claimSetResourceClaimActionAuthStrategies](#schemaclaimsetresourceclaimactionauthstrategies)]¦null|false|none|none| +|children|[[claimSetResourceClaimModel](#schemaclaimsetresourceclaimmodel)]¦null|false|none|Children are collection of ResourceClaim| + +

copyClaimSetRequest

+ + + + + + +```json +{ + "originalId": 0, + "name": "string" +} + +``` + +CopyClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|originalId|integer(int32)|false|none|ClaimSet id to copy| +|name|string|false|none|New claimset name| + +

editApplicationRequest

+ + + + + + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +EditApplicationRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string|false|none|Application name| +|vendorId|integer(int32)|false|none|Vendor/ company id| +|claimSetName|string|false|none|Claim set name| +|profileIds|[integer]¦null|false|none|Profile id| +|educationOrganizationIds|[integer]|false|none|Education organization ids| +|odsInstanceIds|[integer]|false|none|List of ODS instance id| + +

editClaimSetRequest

+ + + + + + +```json +{ + "name": "string" +} + +``` + +EditClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Claim set name| + +

editOdsInstanceContextRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +EditOdsInstanceContextRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|false|none|ODS instance context ODS instance id.| +|contextKey|string|false|none|context key.| +|contextValue|string|false|none|context value.| + +

editOdsInstanceDerivativeRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} + +``` + +EditOdsInstanceDerivativeRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|false|none|ODS instance derivative ODS instance id.| +|derivativeType|string|false|none|derivative type.| +|connectionString|string|false|none|connection string.| + +

editOdsInstanceRequest

+ + + + + + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} + +``` + +EditOdsInstanceRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Ods Instance name| +|instanceType|string¦null|false|none|Ods Instance type| +|connectionString|string¦null|false|none|Ods Instance connection string| + +

editProfileRequest

+ + + + + + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + +``` + +EditProfileRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Profile name| +|definition|string|false|none|Profile definition| + +

editResourceClaimOnClaimSetRequest

+ + + + + + +```json +{ + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} + +``` + +EditResourceClaimActionsOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|resourceClaimActions|[[resourceClaimAction](#schemaresourceclaimaction)]|false|none|none| + +

editVendorRequest

+ + + + + + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +EditVendorRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|company|string|false|none|Vendor/ company name| +|namespacePrefixes|string|false|none|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|string|false|none|Vendor contact name| +|contactEmailAddress|string|false|none|Vendor contact email id| + +

importClaimSetRequest

+ + + + + + +```json +{ + "name": "string", + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} + +``` + +ImportClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|false|none|Claim set name| +|resourceClaims|[[claimSetResourceClaimModel](#schemaclaimsetresourceclaimmodel)]|false|none|Resource Claims| + +

informationResult

+ + + + + + +```json +{ + "version": "string", + "build": "string" +} + +``` + +Information + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|version|string|false|none|Application version| +|build|string|false|none|Build / release version| + +

odsInstanceContextModel

+ + + + + + +```json +{ + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +OdsInstanceContext + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|odsInstanceId|integer(int32)|false|none|none| +|contextKey|string¦null|false|none|none| +|contextValue|string¦null|false|none|none| + +

odsInstanceDerivativeModel

+ + + + + + +```json +{ + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" +} + +``` + +OdsInstanceDerivative + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|odsInstanceId|integer(int32)¦null|false|none|none| +|derivativeType|string¦null|false|none|none| + +

odsInstanceDetailModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string", + "odsInstanceContexts": [ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } + ], + "odsInstanceDerivatives": [ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } + ] +} + +``` + +OdsInstanceDetail + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|instanceType|string¦null|false|none|none| +|odsInstanceContexts|[[odsInstanceContextModel](#schemaodsinstancecontextmodel)]¦null|false|none|none| +|odsInstanceDerivatives|[[odsInstanceDerivativeModel](#schemaodsinstancederivativemodel)]¦null|false|none|none| + +

odsInstanceModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string" +} + +``` + +OdsInstance + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|instanceType|string¦null|false|none|none| + +

overrideAuthStategyOnClaimSetRequest

+ + + + + + +```json +{ + "actionName": "string", + "authorizationStrategies": [ + "string" + ] +} + +``` + +OverrideAuthStategyOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|actionName|string¦null|false|none|none| +|authorizationStrategies|[string]|false|none|AuthorizationStrategy Names| + +

profileDetailsModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "definition": "string" +} + +``` + +ProfileDetails + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|false|none|none| +|name|string¦null|false|none|none| +|definition|string¦null|false|none|none| + +

profileModel

+ + + + + + +```json +{ + "id": 0, + "name": "string" +} + +``` + +Profile + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|false|none|none| +|name|string¦null|false|none|none| + +

registerClientRequest

+ + + + + + +```json +{ + "clientId": "string", + "clientSecret": "string", + "displayName": "string" +} + +``` + +RegisterClientRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|clientId|string|false|none|Client id| +|clientSecret|string|false|none|Client secret| +|displayName|string|false|none|Client display name| + +

resourceClaimAction

+ + + + + + +```json +{ + "name": "string", + "enabled": true +} + +``` + +ResourceClaimAction + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string¦null|false|none|none| +|enabled|boolean|false|none|none| + +

resourceClaimModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [] + } + ] +} + +``` + +ResourceClaimModel + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|parentId|integer(int32)¦null|false|none|none| +|parentName|string¦null|false|none|none| +|children|[[resourceClaimModel](#schemaresourceclaimmodel)]¦null|false|none|Children are collection of SimpleResourceClaimModel| + +

simpleApplicationModel

+ + + + + + +```json +{ + "applicationName": "string" +} + +``` + +Application + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string¦null|false|none|none| + +

vendorModel

+ + + + + + +```json +{ + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +Vendor + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|false|none|none| +|company|string¦null|false|none|none| +|namespacePrefixes|string¦null|false|none|none| +|contactName|string¦null|false|none|none| +|contactEmailAddress|string¦null|false|none|none| + diff --git a/docs/api-specifications/markdown/admin-api-console-2.3.0-pre-summary.md b/docs/api-specifications/markdown/admin-api-console-2.3.0-pre-summary.md new file mode 100644 index 000000000..e12b9b3d8 --- /dev/null +++ b/docs/api-specifications/markdown/admin-api-console-2.3.0-pre-summary.md @@ -0,0 +1,199 @@ + + +

Admin API Documentation vadminconsole

+ +> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu. + +The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API. + +# Authentication + +- oAuth2 authentication. + + - Flow: clientCredentials + + - Token URL = [https://localhost/connect/token](https://localhost/connect/token) + +|Scope|Scope Description| +|---|---| +|edfi_admin_api/full_access|Full access to the Admin API| +|edfi_admin_api/tenant_access|Access to a specific tenant| +|edfi_admin_api/worker|Worker access to the Admin API| + +

Information

+ +## Retrieve API informational metadata + +`GET /` + +> Example responses + +> 200 Response + +```json +{ + "version": "string", + "build": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[informationResult](#schemainformationresult)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|[informationResult](#schemainformationresult)| + + + +

Connect

+ +## Registers new client + +`POST /connect/register` + +Registers new client + +> Body parameter + +```yaml +ClientId: string +ClientSecret: string +DisplayName: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» ClientId|body|string|false|Client id| +|» ClientSecret|body|string|false|Client secret| +|» DisplayName|body|string|false|Client display name| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Application registered successfully.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves bearer token + +`POST /connect/token` + +To authenticate Swagger requests, execute using "Authorize" above, not "Try It Out" here. + +> Body parameter + +```yaml +client_id: null +client_secret: null +grant_type: null +scope: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» client_id|body|string |false|none| +|» client_secret|body|string |false|none| +|» grant_type|body|string |false|none| +|» scope|body|string|false|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Sign-in successful.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request, such as invalid scope.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +# Schemas + +

adminApiError

+ + + + + + +```json +{} + +``` + +AdminApiError + +### Properties + +*None* + +

informationResult

+ + + + + + +```json +{ + "version": "string", + "build": "string" +} + +``` + +Information + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|version|string|false|none|Application version| +|build|string|false|none|Build / release version| + +

registerClientRequest

+ + + + + + +```json +{ + "clientId": "string", + "clientSecret": "string", + "displayName": "string" +} + +``` + +RegisterClientRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|clientId|string|false|none|Client id| +|clientSecret|string|false|none|Client secret| +|displayName|string|false|none|Client display name| + diff --git a/docs/api-specifications/openapi-yaml/admin-api-1.4.3.yaml b/docs/api-specifications/openapi-yaml/admin-api-1.4.3.yaml new file mode 100644 index 000000000..f9af4e3b9 --- /dev/null +++ b/docs/api-specifications/openapi-yaml/admin-api-1.4.3.yaml @@ -0,0 +1,1340 @@ +openapi: 3.0.1 +info: + version: 1.4.3 + title: "Ed-Fi Admin API" + description: The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API. +paths: + /v1/vendors: + get: + tags: + - Vendors + summary: Retrieves all vendors. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/vendorModel' + post: + tags: + - Vendors + summary: Creates vendor based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addVendorRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/vendorModel' + '/v1/vendors/{id}': + get: + tags: + - Vendors + summary: Retrieves a specific vendor based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/vendorModel' + put: + tags: + - Vendors + summary: Updates vendor based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editVendorRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/vendorModel' + delete: + tags: + - Vendors + summary: Deletes an existing vendor using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v1/tenants: + get: + tags: + - Tenants + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + /v1/odsInstances: + get: + tags: + - OdsInstances + summary: Retrieves all odsInstances. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/odsInstanceModel' + post: + tags: + - OdsInstances + summary: Creates odsInstance based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsInstanceRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v1/odsInstances/{id}': + get: + tags: + - OdsInstances + summary: Retrieves a specific odsInstance based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceModel' + put: + tags: + - OdsInstances + summary: Updates odsInstance based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - OdsInstances + summary: Deletes an existing odsInstance using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v1/claimsets: + get: + tags: + - Claimsets + summary: Retrieves all claimsets. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/claimSetModel' + post: + tags: + - Claimsets + summary: Creates claimset based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/request' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/claimSetDetailsModel' + '/v1/claimsets/{id}': + get: + tags: + - Claimsets + summary: Retrieves a specific claimset based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/claimSetDetailsModel' + put: + tags: + - Claimsets + summary: Updates claimset based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/claimSetDetailsModel' + delete: + tags: + - Claimsets + summary: Deletes an existing claimset using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v1/applications: + get: + tags: + - Applications + summary: Retrieves all applications. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/applicationModel' + post: + tags: + - Applications + summary: Creates application based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addApplicationRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/applicationResult' + '/v1/applications/{id}': + get: + tags: + - Applications + summary: Retrieves a specific application based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/applicationModel' + put: + tags: + - Applications + summary: Updates application based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editApplicationRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/applicationModel' + delete: + tags: + - Applications + summary: Deletes an existing application using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + '/v1/vendors/{id}/applications': + get: + tags: + - Vendors + summary: Retrieves applications assigned to a specific vendor based on the resource identifier. + description: Retrieves applications assigned to a specific vendor based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/applicationModel' + /: + get: + tags: + - Information + summary: Retrieve API informational metadata + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/informationResult' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + content: + application/json: + schema: + $ref: '#/components/schemas/informationResult' + /connect/register: + post: + tags: + - Connect + summary: Registers new client + description: Registers new client + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + ClientId: + type: string + description: Client id + ClientSecret: + type: string + description: Client secret + DisplayName: + type: string + description: Client display name + encoding: + ClientId: + style: form + ClientSecret: + style: form + DisplayName: + style: form + responses: + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Application registered successfully. + /connect/token: + post: + tags: + - Connect + summary: Retrieves bearer token + description: "\nTo authenticate Swagger requests, execute using \"Authorize\" above, not \"Try It Out\" here." + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + client_id: + type: string + client_secret: + type: string + grant_type: + type: string + scope: + type: string + responses: + '400': + description: 'Bad request, such as invalid scope.' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Sign-in successful. + '/v1/applications/{id}/reset-credential': + put: + tags: + - Applications + summary: Reset application credentials. Returns new key and secret. + description: Reset application credentials. Returns new key and secret. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/applicationResult' +components: + schemas: + addApplicationRequest: + title: AddApplicationRequest + type: object + properties: + applicationName: + type: string + description: Application name + vendorId: + type: integer + description: Vendor/ company id + format: int32 + claimSetName: + type: string + description: Claim set name + profileId: + type: integer + description: Profile id + format: int32 + nullable: true + odsInstanceId: + type: integer + description: Ods Instance id + format: int32 + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int32 + description: Education organization ids + additionalProperties: false + addOdsInstanceRequest: + title: AddOdsInstanceRequest + type: object + properties: + name: + type: string + description: Ods Instance name + instanceType: + type: string + description: Ods Instance type + nullable: true + status: + type: string + description: Ods Instance status + nullable: true + isExtended: + type: boolean + description: Ods Instance is extended + nullable: true + version: + type: string + description: Ods Instance version + nullable: true + additionalProperties: false + addVendorRequest: + title: AddVendorRequest + type: object + properties: + company: + type: string + description: Vendor/ company name + namespacePrefixes: + type: string + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + contactName: + type: string + description: Vendor contact name + contactEmailAddress: + type: string + description: Vendor contact email id + additionalProperties: false + adminApiError: + title: AdminApiError + type: object + additionalProperties: false + description: Wrapper schema for all error responses + applicationModel: + title: Application + type: object + properties: + applicationId: + type: integer + format: int32 + applicationName: + type: string + nullable: true + claimSetName: + type: string + nullable: true + profileName: + type: string + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int32 + nullable: true + odsInstanceName: + type: string + nullable: true + odsInstanceId: + type: integer + format: int32 + nullable: true + vendorId: + type: integer + format: int32 + nullable: true + profiles: + type: array + items: + $ref: '#/components/schemas/profile' + nullable: true + additionalProperties: false + applicationResult: + title: ApplicationKeySecret + type: object + properties: + applicationId: + type: integer + format: int32 + key: + type: string + nullable: true + secret: + type: string + nullable: true + additionalProperties: false + authorizationStrategiesModel: + title: AuthorizationStrategies + type: object + properties: + authorizationStrategies: + type: array + items: + $ref: '#/components/schemas/authorizationStrategyModel' + nullable: true + additionalProperties: false + authorizationStrategyModel: + title: AuthorizationStrategy + type: object + properties: + authStrategyId: + type: integer + format: int32 + authStrategyName: + type: string + nullable: true + displayName: + type: string + nullable: true + isInheritedFromParent: + type: boolean + additionalProperties: false + childrenRequestResourceClaimModel: + title: ResourceClaim + type: object + properties: + name: + type: string + nullable: true + create: + type: boolean + read: + type: boolean + update: + type: boolean + delete: + type: boolean + readChanges: + type: boolean + authStrategyOverridesForCRUD: + type: array + items: + $ref: '#/components/schemas/authorizationStrategiesModel' + nullable: true + children: + type: array + items: + $ref: '#/components/schemas/requestResourceClaimModel' + nullable: true + additionalProperties: false + claimSetDetailsModel: + title: ClaimSetWithResources + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + isSystemReserved: + type: boolean + applicationsCount: + type: integer + format: int32 + resourceClaims: + type: array + items: + $ref: '#/components/schemas/resourceClaimModel' + nullable: true + additionalProperties: false + claimSetModel: + title: ClaimSet + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + isSystemReserved: + type: boolean + applicationsCount: + type: integer + format: int32 + additionalProperties: false + editApplicationRequest: + title: EditApplicationRequest + type: object + properties: + applicationId: + type: integer + description: Application id + format: int32 + applicationName: + type: string + description: Application name + vendorId: + type: integer + description: Vendor/ company id + format: int32 + claimSetName: + type: string + description: Claim set name + profileId: + type: integer + description: Profile id + format: int32 + nullable: true + odsInstanceId: + type: integer + description: Ods Instance id + format: int32 + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int32 + description: Education organization ids + additionalProperties: false + editClaimSetRequest: + title: EditClaimSetRequest + type: object + properties: + id: + type: integer + description: ClaimSet id + format: int32 + name: + type: string + description: Claim set name + resourceClaims: + type: array + items: + $ref: '#/components/schemas/requestResourceClaimModel' + description: Resource Claims + additionalProperties: false + editOdsInstanceRequest: + title: EditOdsInstanceRequest + type: object + properties: + odsInstanceId: + type: integer + description: Ods Instance id + format: int32 + name: + type: string + description: Ods Instance name + instanceType: + type: string + description: Ods Instance type + nullable: true + status: + type: string + description: Ods Instance status + nullable: true + isExtended: + type: boolean + description: Ods Instance is extended + nullable: true + version: + type: string + description: Ods Instance version + nullable: true + additionalProperties: false + editVendorRequest: + title: EditVendorRequest + type: object + properties: + vendorId: + type: integer + description: Vendor/ company id + format: int32 + company: + type: string + description: Vendor/ company name + namespacePrefixes: + type: string + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + contactName: + type: string + description: Vendor contact name + contactEmailAddress: + type: string + description: Vendor contact email id + additionalProperties: false + informationResult: + title: Information + type: object + properties: + version: + type: string + description: Application version + build: + type: string + description: Build / release version + additionalProperties: false + odsInstanceModel: + title: OdsInstance + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + instanceType: + type: string + nullable: true + version: + type: string + nullable: true + isExtended: + type: boolean + nullable: true + status: + type: string + nullable: true + additionalProperties: false + profile: + title: Profile + type: object + properties: + id: + type: integer + format: int32 + nullable: true + additionalProperties: false + registerClientRequest: + title: RegisterClientRequest + type: object + properties: + clientId: + type: string + description: Client id + clientSecret: + type: string + description: Client secret + displayName: + type: string + description: Client display name + additionalProperties: false + request: + title: AddClaimSetRequest + type: object + properties: + name: + type: string + description: Claim set name + resourceClaims: + type: array + items: + $ref: '#/components/schemas/requestResourceClaimModel' + description: Resource Claims + additionalProperties: false + requestResourceClaimModel: + title: ResourceClaim + type: object + properties: + name: + type: string + nullable: true + create: + type: boolean + read: + type: boolean + update: + type: boolean + delete: + type: boolean + readChanges: + type: boolean + authStrategyOverridesForCRUD: + type: array + items: + $ref: '#/components/schemas/authorizationStrategiesModel' + nullable: true + children: + type: array + items: + $ref: '#/components/schemas/childrenRequestResourceClaimModel' + description: Children are collection of ResourceClaim + nullable: true + additionalProperties: false + resourceClaimModel: + title: ResourceClaim + type: object + properties: + name: + type: string + nullable: true + create: + type: boolean + read: + type: boolean + update: + type: boolean + delete: + type: boolean + readChanges: + type: boolean + defaultAuthStrategiesForCRUD: + type: array + items: + $ref: '#/components/schemas/authorizationStrategiesModel' + nullable: true + authStrategyOverridesForCRUD: + type: array + items: + $ref: '#/components/schemas/authorizationStrategiesModel' + nullable: true + children: + type: array + items: + $ref: '#/components/schemas/resourceClaimModel' + description: Children are collection of ResourceClaim + nullable: true + additionalProperties: false + vendorModel: + title: Vendor + type: object + properties: + vendorId: + type: integer + format: int32 + nullable: true + company: + type: string + nullable: true + namespacePrefixes: + type: string + nullable: true + contactName: + type: string + nullable: true + contactEmailAddress: + type: string + nullable: true + additionalProperties: false + securitySchemes: + oauth: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://localhost/connect/token + scopes: + edfi_admin_api/full_access: Full access to the Admin API +security: + - oauth: + - api \ No newline at end of file diff --git a/docs/api-specifications/openapi-yaml/admin-api-2.2.0.yaml b/docs/api-specifications/openapi-yaml/admin-api-2.2.0.yaml new file mode 100644 index 000000000..7329c18dc --- /dev/null +++ b/docs/api-specifications/openapi-yaml/admin-api-2.2.0.yaml @@ -0,0 +1,2383 @@ +openapi: 3.0.1 +info: + version: 2.2.0 + description: The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API. +paths: + /v2/resourceClaims: + get: + tags: + - ResourceClaims + summary: Retrieves all resourceClaims. + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/resourceClaim' + '/v2/resourceClaims/{id}': + get: + tags: + - ResourceClaims + summary: Retrieves a specific resourceClaim based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/resourceClaim' + /v2/vendors: + get: + tags: + - Vendors + summary: Retrieves all vendors. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + required: true + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + required: true + schema: + type: integer + format: int32 + default: '25' + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/vendor' + post: + tags: + - Vendors + summary: Creates vendor based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addVendorRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/vendors/{id}': + get: + tags: + - Vendors + summary: Retrieves a specific vendor based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/vendor' + put: + tags: + - Vendors + summary: Updates vendor based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editVendorRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - Vendors + summary: Deletes an existing vendor using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/profiles: + get: + tags: + - Profiles + summary: Retrieves all profiles. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + required: true + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + required: true + schema: + type: integer + format: int32 + default: '25' + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/profile' + post: + tags: + - Profiles + summary: Creates profile based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addProfileRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/profiles/{id}': + get: + tags: + - Profiles + summary: Retrieves a specific profile based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/profileDetails' + put: + tags: + - Profiles + summary: Updates profile based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editProfileRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - Profiles + summary: Deletes an existing profile using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstances: + get: + tags: + - OdsInstances + summary: Retrieves all odsInstances. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + required: true + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + required: true + schema: + type: integer + format: int32 + default: '25' + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + + type: array + items: + $ref: '#/components/schemas/odsInstance' + post: + tags: + - OdsInstances + summary: Creates odsInstance based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsIntanceRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstances/{id}': + get: + tags: + - OdsInstances + summary: Retrieves a specific odsInstance based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceDetail' + put: + tags: + - OdsInstances + summary: Updates odsInstance based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - OdsInstances + summary: Deletes an existing odsInstance using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstanceDerivatives: + get: + tags: + - OdsInstanceDerivatives + summary: Retrieves all odsInstanceDerivatives. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + required: true + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + required: true + schema: + type: integer + format: int32 + default: '25' + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + + type: array + items: + $ref: '#/components/schemas/odsInstanceDerivative' + post: + tags: + - OdsInstanceDerivatives + summary: Creates odsInstanceDerivative based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsInstanceDerivateRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstanceDerivatives/{id}': + get: + tags: + - OdsInstanceDerivatives + summary: Retrieves a specific odsInstanceDerivative based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceDerivative' + put: + tags: + - OdsInstanceDerivatives + summary: Updates odsInstanceDerivative based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceDerivateRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - OdsInstanceDerivatives + summary: Deletes an existing odsInstanceDerivative using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstanceContexts: + get: + tags: + - OdsInstanceContexts + summary: Retrieves all odsInstanceContexts. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + required: true + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + required: true + schema: + type: integer + format: int32 + default: '25' + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + + type: array + items: + $ref: '#/components/schemas/odsInstanceContext' + post: + tags: + - OdsInstanceContexts + summary: Creates odsInstanceContext based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsInstanceContextRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstanceContexts/{id}': + get: + tags: + - OdsInstanceContexts + summary: Retrieves a specific odsInstanceContext based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceContext' + put: + tags: + - OdsInstanceContexts + summary: Updates odsInstanceContext based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceContextRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - OdsInstanceContexts + summary: Deletes an existing odsInstanceContext using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + '/v2/claimSets/{id}/export': + get: + tags: + - ClaimSets + summary: Exports a specific claimSet based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/claimsetWithResources' + /v2/claimSets: + get: + tags: + - ClaimSets + summary: Retrieves all claimSets. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + required: true + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + required: true + schema: + type: integer + format: int32 + default: '25' + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/claimset' + post: + tags: + - ClaimSets + summary: Creates claimSet based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addClaimsetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/claimSets/{id}': + get: + tags: + - ClaimSets + summary: Retrieves a specific claimSet based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/claimsetWithResources' + put: + tags: + - ClaimSets + summary: Updates claimSet based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editClaimsetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - ClaimSets + summary: Deletes an existing claimSet using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/authorizationStrategies: + get: + tags: + - AuthorizationStrategies + summary: Retrieves all authorizationStrategies. + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/authorizationStrategy' + /v2/applications: + get: + tags: + - Applications + summary: Retrieves all applications. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + required: true + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + required: true + schema: + type: integer + format: int32 + default: '25' + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + + type: array + items: + $ref: '#/components/schemas/application' + post: + tags: + - Applications + summary: Creates application based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addApplicationRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/applicationResult' + '/v2/applications/{id}': + get: + tags: + - Applications + summary: Retrieves a specific application based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/application' + put: + tags: + - Applications + summary: Updates application based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editApplicationResult' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - Applications + summary: Deletes an existing application using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + '/v2/odsInstances/{id}/applications': + get: + tags: + - OdsInstances + summary: Retrieves applications assigned to a specific ODS instance based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/application' + '/v2/vendors/{id}/applications': + get: + tags: + - Vendors + summary: Retrieves applications assigned to a specific vendor based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/application' + /v2/actions: + get: + tags: + - Actions + summary: Retrieves all actions. + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/action' + /: + get: + tags: + - Information + summary: Retrieve API informational metadata + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/information' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + content: + application/json: + schema: + $ref: '#/components/schemas/information' + /v2/claimSets/copy: + post: + tags: + - ClaimSets + summary: Copies the existing claimset and create new one. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/copyClaimsetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + /v2/claimSets/import: + post: + tags: + - ClaimSets + summary: Imports a new claimSet based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/importClaimsetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/overrideAuthorizationStrategy': + post: + tags: + - ClaimSets + summary: Overrides the default authorization strategies on provided resource claim for a specific action. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/overrideAuthorizationStrategyOnClaimsetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/resetAuthorizationStrategies': + post: + tags: + - ClaimSets + summary: Resets to default authorization strategies on provided resource claim. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + '/v2/claimSets/{claimSetId}/resourceClaimActions': + post: + tags: + - ClaimSets + summary: Adds resourceClaimAction association to a claimSet. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addResourceClaimActionsOnClaimsetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + /connect/register: + post: + tags: + - Connect + summary: Registers new client + description: Registers new client + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + ClientId: + type: string + description: Client id + ClientSecret: + type: string + description: Client secret + DisplayName: + type: string + description: Client display name + encoding: + ClientId: + style: form + ClientSecret: + style: form + DisplayName: + style: form + responses: + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Application registered successfully. + /connect/token: + post: + tags: + - Connect + summary: Retrieves bearer token + description: "\nTo authenticate Swagger requests, execute using \"Authorize\" above, not \"Try It Out\" here." + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + client_id: + type: string + client_secret: + type: string + grant_type: + type: string + scope: + type: string + responses: + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Sign-in successful. + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}': + put: + tags: + - ClaimSets + summary: Updates the resourceClaimAction to a specific resourceClaim on a claimSet. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editResourceClaimActionsOnClaimsetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - ClaimSets + summary: Deletes a resourceClaims association from a claimSet. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + '/v2/applications/{id}/reset-credential': + put: + tags: + - Applications + summary: Reset application credentials. Returns new key and secret. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/applicationResult' +components: + schemas: + action: + title: Action + required: + - id + - name + - uri + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + uri: + type: string + nullable: true + additionalProperties: false + adminApiError: + title: AdminApiError + required: + - errors + - title + type: object + properties: + title: + type: string + nullable: true + readOnly: true + errors: + type: array + items: + type: string + nullable: true + readOnly: true + additionalProperties: false + description: Wrapper schema for all error responses + addApplicationRequest: + title: AddApplicationRequest + required: + - applicationName + - claimSetName + - educationOrganizationIds + - odsInstanceIds + - vendorId + type: object + properties: + applicationName: + type: string + description: Application name + vendorId: + type: integer + description: Vendor/ company id + format: int32 + claimSetName: + type: string + description: Claim set name + profileIds: + type: array + items: + type: integer + format: int32 + description: Profile id + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int32 + description: Education organization ids + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + additionalProperties: false + application: + title: Application + required: + - applicationName + - claimSetName + - educationOrganizationIds + - id + - odsInstanceIds + - profileIds + - vendorId + type: object + properties: + id: + type: integer + format: int32 + applicationName: + type: string + nullable: true + claimSetName: + type: string + nullable: true + educationOrganizationIds: + required: + - item + type: array + items: + type: integer + format: int64 + nullable: true + vendorId: + type: integer + format: int32 + nullable: true + profileIds: + required: + - item + type: array + items: + type: integer + format: int32 + nullable: true + odsInstanceIds: + required: + - item + type: array + items: + type: integer + format: int32 + nullable: true + additionalProperties: false + applicationResult: + title: ApplicationKeySecret + required: + - id + - key + - secret + type: object + properties: + id: + type: integer + format: int32 + key: + type: string + nullable: true + secret: + type: string + nullable: true + additionalProperties: false + editApplicationResult: + title: EditApplicationRequest + required: + - applicationName + - claimSetName + - educationOrganizationIds + - id + - odsInstanceIds + - vendorId + type: object + properties: + applicationName: + type: string + description: Application name + vendorId: + type: integer + description: Vendor/ company id + format: int32 + claimSetName: + type: string + description: Claim set name + profileIds: + type: array + items: + type: integer + format: int32 + description: Profile id + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int32 + description: Education organization ids + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + additionalProperties: false + simpleApplication: + title: Application + required: + - applicationName + type: object + properties: + applicationName: + type: string + nullable: true + additionalProperties: false + authorizationStrategy: + title: AuthorizationStrategy + required: + - authStrategyId + - authStrategyName + - displayName + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + displayName: + type: string + nullable: true + additionalProperties: false + addClaimsetRequest: + title: AddClaimSetRequest + required: + - name + type: object + properties: + name: + type: string + description: Claim set name + additionalProperties: false + claimsetResourceClaim: + title: ClaimSetResourceClaim + required: + - actions + - authorizationStrategyOverridesForCRUD + - children + - defaultAuthorizationStrategiesForCRUD + - id + - name + type: object + properties: + id: + type: integer + format: int32 + readOnly: true + name: + type: string + nullable: true + actions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + nullable: true + _defaultAuthorizationStrategiesForCRUD: + type: array + items: + $ref: '#/components/schemas/claimsetResourceClaimActionAuthorizationStrategies' + nullable: true + readOnly: true + authorizationStrategyOverridesForCRUD: + type: array + items: + $ref: '#/components/schemas/claimsetResourceClaimActionAuthorizationStrategies' + nullable: true + children: + type: array + items: + $ref: '#/components/schemas/claimsetResourcesClaim' + description: Children are collection of ResourceClaim + nullable: true + additionalProperties: false + claimsetWithResources: + title: ClaimSetWithResources + required: + - applications + - id + - isSystemReserved + - name + - resourceClaims + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + _isSystemReserved: + type: boolean + readOnly: true + _applications: + type: array + items: + $ref: '#/components/schemas/simpleApplication' + nullable: true + readOnly: true + resourceClaims: + type: array + items: + $ref: '#/components/schemas/claimsetResourcesClaim' + nullable: true + additionalProperties: false + claimset: + title: ClaimSet + required: + - applications + - id + - isSystemReserved + - name + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + _isSystemReserved: + type: boolean + readOnly: true + _applications: + type: array + items: + $ref: '#/components/schemas/simpleApplication' + nullable: true + readOnly: true + additionalProperties: false + claimsetResourcesClaim: + title: ClaimSetResourceClaim + required: + - actions + - authorizationStrategyOverridesForCRUD + - children + - defaultAuthorizationStrategiesForCRUD + - id + - name + type: object + properties: + id: + type: integer + format: int32 + readOnly: true + name: + type: string + nullable: true + actions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + nullable: true + _defaultAuthorizationStrategiesForCRUD: + type: array + items: + $ref: '#/components/schemas/claimsetResourceClaimActionAuthorizationStrategies' + nullable: true + readOnly: true + authorizationStrategyOverridesForCRUD: + type: array + items: + $ref: '#/components/schemas/claimsetResourceClaimActionAuthorizationStrategies' + nullable: true + children: + + type: array + items: + $ref: '#/components/schemas/claimsetResourceClaim' + description: Children are collection of ResourceClaim + nullable: true + additionalProperties: false + copyClaimsetRequest: + title: CopyClaimSetRequest + required: + - name + - originalId + type: object + properties: + originalId: + type: integer + description: ClaimSet id to copy + format: int32 + name: + type: string + description: New claimset name + additionalProperties: false + editClaimsetRequest: + title: EditClaimSetRequest + required: + - id + - name + type: object + properties: + name: + type: string + description: Claim set name + additionalProperties: false + importClaimsetRequest: + title: ImportClaimSetRequest + required: + - name + - resourceClaims + type: object + properties: + name: + type: string + description: Claim set name + resourceClaims: + type: array + items: + $ref: '#/components/schemas/claimsetResourcesClaim' + description: Resource Claims + additionalProperties: false + resourceClaim: + title: ResourceClaimModel + required: + - children + - id + - name + - parentId + - parentName + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + parentId: + type: integer + format: int32 + nullable: true + parentName: + type: string + nullable: true + children: + type: array + items: + $ref: '#/components/schemas/resourceClaim' + description: Children are collection of SimpleResourceClaimModel + nullable: true + additionalProperties: false + overrideAuthorizationStrategyOnClaimsetRequest: + title: OverrideAuthStategyOnClaimSetRequest + required: + - actionName + - authorizationStrategies + - authStrategyIds + - claimSetId + - resourceClaimId + type: object + properties: + actionName: + type: string + nullable: true + authorizationStrategies: + type: array + items: + type: string + description: AuthorizationStrategy Names + additionalProperties: false + addResourceClaimActionsOnClaimsetRequest: + title: AddResourceClaimActionsOnClaimSetRequest + required: + - claimSetId + - resourceClaimActions + - resourceClaimId + type: object + properties: + resourceClaimId: + type: integer + description: ResourceClaim id + format: int32 + resourceClaimActions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + additionalProperties: false + editResourceClaimActionsOnClaimsetRequest: + title: EditResourceClaimActionsOnClaimSetRequest + required: + - claimSetId + - resourceClaimActions + - resourceClaimId + type: object + properties: + resourceClaimActions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + additionalProperties: false + registerClientRequest: + title: RegisterClientRequest + required: + - clientId + - clientSecret + - displayName + type: object + properties: + clientId: + type: string + description: Client id + clientSecret: + type: string + description: Client secret + displayName: + type: string + description: Client display name + additionalProperties: false + information: + title: Information + required: + - build + - version + type: object + properties: + version: + type: string + description: Application version + build: + type: string + description: Build / release version + additionalProperties: false + odsInstanceDetail: + title: OdsInstanceDetail + required: + - instanceType + - name + - odsInstanceContexts + - odsInstanceDerivatives + - odsInstanceId + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + instanceType: + type: string + nullable: true + odsInstanceContexts: + type: array + items: + $ref: '#/components/schemas/odsInstanceContext' + nullable: true + odsInstanceDerivatives: + type: array + items: + $ref: '#/components/schemas/odsInstanceDerivative' + nullable: true + additionalProperties: false + odsInstance: + title: OdsInstance + required: + - instanceType + - name + - odsInstanceId + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + instanceType: + type: string + nullable: true + additionalProperties: false + addOdsInstanceContextRequest: + title: AddOdsInstanceContextRequest + required: + - contextKey + - contextValue + - odsInstanceId + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance context ODS instance id. + format: int32 + contextKey: + type: string + description: context key. + contextValue: + type: string + description: context value. + additionalProperties: false + editOdsInstanceContextRequest: + title: EditOdsInstanceContextRequest + required: + - contextKey + - contextValue + - id + - odsInstanceId + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance context ODS instance id. + format: int32 + contextKey: + type: string + description: context key. + contextValue: + type: string + description: context value. + additionalProperties: false + odsInstanceContext: + title: OdsInstanceContext + required: + - contextKey + - contextValue + - odsInstanceContextId + - odsInstanceId + type: object + properties: + id: + type: integer + format: int32 + odsInstanceId: + type: integer + format: int32 + contextKey: + type: string + nullable: true + contextValue: + type: string + nullable: true + additionalProperties: false + addOdsInstanceDerivateRequest: + title: AddOdsInstanceDerivativeRequest + required: + - connectionString + - derivativeType + - odsInstanceId + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance derivative ODS instance id. + format: int32 + derivativeType: + type: string + description: derivative type. + connectionString: + type: string + description: connection string. + additionalProperties: false + editOdsInstanceDerivateRequest: + title: EditOdsInstanceDerivativeRequest + required: + - connectionString + - derivativeType + - id + - odsInstanceId + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance derivative ODS instance id. + format: int32 + derivativeType: + type: string + description: derivative type. + connectionString: + type: string + description: connection string. + additionalProperties: false + odsInstanceDerivative: + title: OdsInstanceDerivative + required: + - derivativeType + - id + - odsInstanceId + type: object + properties: + id: + type: integer + format: int32 + odsInstanceId: + type: integer + format: int32 + nullable: true + derivativeType: + type: string + nullable: true + additionalProperties: false + addOdsIntanceRequest: + title: AddOdsInstanceRequest + required: + - connectionString + - instanceType + - name + type: object + properties: + name: + type: string + description: Ods Instance name + instanceType: + type: string + description: Ods Instance type + connectionString: + type: string + description: Ods Instance connection string + additionalProperties: false + editOdsInstanceRequest: + title: EditOdsInstanceRequest + required: + - connectionString + - id + - instanceType + - name + type: object + properties: + name: + type: string + description: Ods Instance name + instanceType: + type: string + description: Ods Instance type + connectionString: + type: string + description: Ods Instance connection string + nullable: true + additionalProperties: false + addProfileRequest: + title: AddProfileRequest + required: + - definition + - name + type: object + properties: + name: + type: string + description: Profile name + definition: + type: string + description: Profile definition + additionalProperties: false + example: "{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + editProfileRequest: + title: EditProfileRequest + required: + - definition + - id + - name + type: object + properties: + name: + type: string + description: Profile name + definition: + type: string + description: Profile definition + additionalProperties: false + example: "{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + profileDetails: + title: ProfileDetails + required: + - definition + - id + - name + type: object + properties: + id: + type: integer + format: int32 + nullable: true + name: + type: string + nullable: true + definition: + type: string + nullable: true + additionalProperties: false + profile: + title: Profile + required: + - id + - name + type: object + properties: + id: + type: integer + format: int32 + nullable: true + name: + type: string + nullable: true + additionalProperties: false + addVendorRequest: + title: AddVendorRequest + required: + - company + - contactEmailAddress + - contactName + - namespacePrefixes + type: object + properties: + company: + type: string + description: Vendor/ company name + namespacePrefixes: + type: string + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + contactName: + type: string + description: Vendor contact name + contactEmailAddress: + type: string + description: Vendor contact email id + additionalProperties: false + editVendorRequest: + title: EditVendorRequest + required: + - company + - contactEmailAddress + - contactName + - id + - namespacePrefixes + type: object + properties: + company: + type: string + description: Vendor/ company name + namespacePrefixes: + type: string + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + contactName: + type: string + description: Vendor contact name + contactEmailAddress: + type: string + description: Vendor contact email id + additionalProperties: false + vendor: + title: Vendor + required: + - company + - contactEmailAddress + - contactName + - id + - namespacePrefixes + type: object + properties: + id: + type: integer + format: int32 + nullable: true + company: + type: string + nullable: true + namespacePrefixes: + type: string + nullable: true + contactName: + type: string + nullable: true + contactEmailAddress: + type: string + nullable: true + additionalProperties: false + resourceClaimAuthorizationStrategy: + title: ResourceClaimAuthorizationStrategy + required: + - authStrategyId + - authStrategyName + - isInheritedFromParent + type: object + properties: + authStrategyId: + type: integer + format: int32 + authStrategyName: + type: string + nullable: true + isInheritedFromParent: + type: boolean + additionalProperties: false + claimsetResourceClaimActionAuthorizationStrategies: + title: ClaimSetResourceClaimActionAuthorizationStrategies + required: + - actionId + - actionName + - authorizationStrategies + type: object + properties: + actionId: + type: integer + format: int32 + nullable: true + actionName: + type: string + nullable: true + authorizationStrategies: + type: array + items: + $ref: '#/components/schemas/resourceClaimAuthorizationStrategy' + nullable: true + additionalProperties: false + resourceClaimAction: + title: ResourceClaimAction + required: + - enabled + - name + type: object + properties: + name: + type: string + nullable: true + enabled: + type: boolean + additionalProperties: false + securitySchemes: + oauth: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://localhost/adminapi/connect/token + scopes: + edfi_admin_api/full_access: Unrestricted access to all Admin API endpoints +security: + - oauth: + - api diff --git a/docs/api-specifications/openapi-yaml/admin-api-2.2.1.yaml b/docs/api-specifications/openapi-yaml/admin-api-2.2.1.yaml new file mode 100644 index 000000000..1c58a36e0 --- /dev/null +++ b/docs/api-specifications/openapi-yaml/admin-api-2.2.1.yaml @@ -0,0 +1,2586 @@ +openapi: 3.0.1 +info: + title: Admin API Documentation + description: 'The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API.' + version: 2.2.1 +paths: + /v2/resourceClaims: + get: + tags: + - ResourceClaims + summary: Retrieves all resourceClaims. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Resource Claim Id + schema: + type: integer + format: int32 + - name: name + in: query + description: Resource Claim Name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/resourceClaimModel' + '/v2/resourceClaims/{id}': + get: + tags: + - ResourceClaims + summary: Retrieves a specific resourceClaim based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/resourceClaimModel' + /v2/vendors: + get: + tags: + - Vendors + summary: Retrieves all vendors. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Vendor/ company id + schema: + type: integer + format: int32 + - name: company + in: query + description: Vendor/ company name + schema: + type: string + - name: namespacePrefixes + in: query + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + schema: + type: string + - name: contactName + in: query + description: Vendor contact name + schema: + type: string + - name: contactEmailAddress + in: query + description: Vendor contact email id + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/vendorModel' + post: + tags: + - Vendors + summary: Creates vendor based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addVendorRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/vendors/{id}': + get: + tags: + - Vendors + summary: Retrieves a specific vendor based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/vendorModel' + put: + tags: + - Vendors + summary: Updates vendor based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editVendorRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - Vendors + summary: Deletes an existing vendor using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/profiles: + get: + tags: + - Profiles + summary: Retrieves all profiles. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Profile id + schema: + type: integer + format: int32 + - name: name + in: query + description: Profile name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/profileModel' + post: + tags: + - Profiles + summary: Creates profile based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addProfileRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/profiles/{id}': + get: + tags: + - Profiles + summary: Retrieves a specific profile based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/profileDetailsModel' + put: + tags: + - Profiles + summary: Updates profile based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editProfileRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - Profiles + summary: Deletes an existing profile using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstances: + get: + tags: + - OdsInstances + summary: Retrieves all odsInstances. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: List of ODS instance id + schema: + type: integer + format: int32 + - name: name + in: query + description: Ods Instance name + schema: + type: string + - name: instanceType + in: query + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/odsInstanceModel' + post: + tags: + - OdsInstances + summary: Creates odsInstance based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsInstanceRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstances/{id}': + get: + tags: + - OdsInstances + summary: Retrieves a specific odsInstance based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceDetailModel' + put: + tags: + - OdsInstances + summary: Updates odsInstance based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - OdsInstances + summary: Deletes an existing odsInstance using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstanceDerivatives: + get: + tags: + - OdsInstanceDerivatives + summary: Retrieves all odsInstanceDerivatives. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/odsInstanceDerivativeModel' + post: + tags: + - OdsInstanceDerivatives + summary: Creates odsInstanceDerivative based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsInstanceDerivativeRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstanceDerivatives/{id}': + get: + tags: + - OdsInstanceDerivatives + summary: Retrieves a specific odsInstanceDerivative based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceDerivativeModel' + put: + tags: + - OdsInstanceDerivatives + summary: Updates odsInstanceDerivative based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceDerivativeRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - OdsInstanceDerivatives + summary: Deletes an existing odsInstanceDerivative using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstanceContexts: + get: + tags: + - OdsInstanceContexts + summary: Retrieves all odsInstanceContexts. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/odsInstanceContextModel' + post: + tags: + - OdsInstanceContexts + summary: Creates odsInstanceContext based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsInstanceContextRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstanceContexts/{id}': + get: + tags: + - OdsInstanceContexts + summary: Retrieves a specific odsInstanceContext based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceContextModel' + put: + tags: + - OdsInstanceContexts + summary: Updates odsInstanceContext based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceContextRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - OdsInstanceContexts + summary: Deletes an existing odsInstanceContext using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + '/v2/claimSets/{id}/export': + get: + tags: + - ClaimSets + summary: Exports a specific claimset by id + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/claimSetDetailsModel' + /v2/claimSets: + get: + tags: + - ClaimSets + summary: Retrieves all claimSets. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Claim set id + schema: + type: integer + format: int32 + - name: name + in: query + description: Claim set name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/claimSetModel' + post: + tags: + - ClaimSets + summary: Creates claimSet based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/claimSets/{id}': + get: + tags: + - ClaimSets + summary: Retrieves a specific claimSet based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/claimSetDetailsModel' + put: + tags: + - ClaimSets + summary: Updates claimSet based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - ClaimSets + summary: Deletes an existing claimSet using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/authorizationStrategies: + get: + tags: + - AuthorizationStrategies + summary: Retrieves all authorizationStrategies. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/authorizationStrategyModel' + /v2/applications: + get: + tags: + - Applications + summary: Retrieves all applications. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Application id + schema: + type: integer + format: int32 + - name: applicationName + in: query + description: Application name + schema: + type: string + - name: claimsetName + in: query + description: Claim set name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/applicationModel' + post: + tags: + - Applications + summary: Creates application based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addApplicationRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/applicationResult' + '/v2/applications/{id}': + get: + tags: + - Applications + summary: Retrieves a specific application based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/applicationModel' + put: + tags: + - Applications + summary: Updates application based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editApplicationRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - Applications + summary: Deletes an existing application using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + '/v2/odsInstances/{id}/applications': + get: + tags: + - OdsInstances + summary: Retrieves applications assigned to a specific ODS instance based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/applicationModel' + '/v2/vendors/{id}/applications': + get: + tags: + - Vendors + summary: Retrieves applications assigned to a specific vendor based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/applicationModel' + /v2/actions: + get: + tags: + - Actions + summary: Retrieves all actions. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Action id + schema: + type: integer + format: int32 + - name: name + in: query + description: Action name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/actionModel' + /: + get: + tags: + - Information + summary: Retrieve API informational metadata + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/informationResult' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + content: + application/json: + schema: + $ref: '#/components/schemas/informationResult' + /v2/claimSets/copy: + post: + tags: + - ClaimSets + summary: Copies the existing claimset and create a new one. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/copyClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + /v2/claimSets/import: + post: + tags: + - ClaimSets + summary: Imports a new claimset + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/importClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/overrideAuthorizationStrategy': + post: + tags: + - ClaimSets + summary: Overrides the default authorization strategies on provided resource claim for a specific action. + description: "Override the default authorization strategies on provided resource claim for a specific action.\r\n\r\nex: actionName = read, authorizationStrategies= [ \"Ownershipbased\" ]" + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/overrideAuthStategyOnClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/resetAuthorizationStrategies': + post: + tags: + - ClaimSets + summary: Resets to default authorization strategies on provided resource claim. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + '/v2/claimSets/{claimSetId}/resourceClaimActions': + post: + tags: + - ClaimSets + summary: Adds ResourceClaimAction association to a claim set. + description: "Add resourceClaimAction association to claim set. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges.\r\nresouceclaimId is required fields." + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addResourceClaimOnClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + /connect/register: + post: + tags: + - Connect + summary: Registers new client + description: Registers new client + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + ClientId: + type: string + description: Client id + ClientSecret: + type: string + description: Client secret + DisplayName: + type: string + description: Client display name + encoding: + ClientId: + style: form + ClientSecret: + style: form + DisplayName: + style: form + responses: + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Application registered successfully. + /connect/token: + post: + tags: + - Connect + summary: Retrieves bearer token + description: "\nTo authenticate Swagger requests, execute using \"Authorize\" above, not \"Try It Out\" here." + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + client_id: + type: 'string' + client_secret: + type: 'string' + grant_type: + type: 'string' + scope: + type: string + responses: + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Sign-in successful. + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}': + put: + tags: + - ClaimSets + summary: Updates the ResourceClaimActions to a specific resource claim on a claimset. + description: 'Updates the resourceClaimActions to a specific resource claim on a claimset. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges.' + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editResourceClaimOnClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - ClaimSets + summary: Deletes a resource claims association from a claimset + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + '/v2/applications/{id}/reset-credential': + put: + tags: + - Applications + summary: Reset application credentials. Returns new key and secret. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/applicationResult' +components: + schemas: + actionModel: + title: Action + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + uri: + type: string + nullable: true + additionalProperties: false + addApplicationRequest: + title: AddApplicationRequest + type: object + properties: + applicationName: + type: string + description: Application name + vendorId: + type: integer + description: Vendor/ company id + format: int32 + claimSetName: + type: string + description: Claim set name + profileIds: + type: array + items: + type: integer + format: int32 + description: Profile id + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + description: Education organization ids + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + additionalProperties: false + addClaimSetRequest: + title: AddClaimSetRequest + type: object + properties: + name: + type: string + description: Claim set name + additionalProperties: false + addOdsInstanceContextRequest: + title: AddOdsInstanceContextRequest + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance context ODS instance id. + format: int32 + contextKey: + type: string + description: context key. + contextValue: + type: string + description: context value. + additionalProperties: false + addOdsInstanceDerivativeRequest: + title: AddOdsInstanceDerivativeRequest + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance derivative ODS instance id. + format: int32 + derivativeType: + type: string + description: derivative type. + connectionString: + type: string + description: connection string. + additionalProperties: false + addOdsInstanceRequest: + title: AddOdsInstanceRequest + type: object + properties: + name: + type: string + description: Ods Instance name + instanceType: + type: string + description: Ods Instance type + nullable: true + connectionString: + type: string + description: Ods Instance connection string + additionalProperties: false + addProfileRequest: + title: AddProfileRequest + type: object + properties: + name: + type: string + description: Profile name + definition: + type: string + description: Profile definition + additionalProperties: false + example: "{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + addResourceClaimOnClaimSetRequest: + title: AddResourceClaimActionsOnClaimSetRequest + type: object + properties: + resourceClaimId: + type: integer + description: ResourceClaim id + format: int32 + resourceClaimActions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + additionalProperties: false + addVendorRequest: + title: AddVendorRequest + type: object + properties: + company: + type: string + description: Vendor/ company name + namespacePrefixes: + type: string + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + contactName: + type: string + description: Vendor contact name + contactEmailAddress: + type: string + description: Vendor contact email id + additionalProperties: false + adminApiError: + title: AdminApiError + type: object + properties: + title: + type: string + nullable: true + readOnly: true + errors: + type: array + items: + type: string + nullable: true + readOnly: true + additionalProperties: false + description: Wrapper schema for all error responses + applicationModel: + title: Application + type: object + properties: + id: + type: integer + format: int32 + applicationName: + type: string + nullable: true + claimSetName: + type: string + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + nullable: true + vendorId: + type: integer + format: int32 + nullable: true + profileIds: + type: array + items: + type: integer + format: int32 + nullable: true + odsInstanceIds: + type: array + items: + type: integer + format: int32 + nullable: true + additionalProperties: false + applicationResult: + title: ApplicationKeySecret + type: object + properties: + id: + type: integer + format: int32 + key: + type: string + nullable: true + secret: + type: string + nullable: true + additionalProperties: false + authorizationStrategy: + title: ResourceClaimAuthorizationStrategy + type: object + properties: + authStrategyId: + type: integer + format: int32 + authStrategyName: + type: string + nullable: true + isInheritedFromParent: + type: boolean + additionalProperties: false + authorizationStrategyModel: + title: AuthorizationStrategy + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + displayName: + type: string + nullable: true + additionalProperties: false + claimSetDetailsModel: + title: ClaimSetWithResources + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + _isSystemReserved: + type: boolean + readOnly: true + _applications: + type: array + items: + $ref: '#/components/schemas/simpleApplicationModel' + nullable: true + readOnly: true + resourceClaims: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimModel' + nullable: true + additionalProperties: false + claimSetModel: + title: ClaimSet + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + _isSystemReserved: + type: boolean + readOnly: true + _applications: + type: array + items: + $ref: '#/components/schemas/simpleApplicationModel' + nullable: true + readOnly: true + additionalProperties: false + claimSetResourceClaimActionAuthStrategies: + title: ClaimSetResourceClaimActionAuthorizationStrategies + type: object + properties: + actionId: + type: integer + format: int32 + nullable: true + actionName: + type: string + nullable: true + authorizationStrategies: + type: array + items: + $ref: '#/components/schemas/authorizationStrategy' + nullable: true + additionalProperties: false + claimSetResourceClaimModel: + title: ClaimSetResourceClaim + type: object + properties: + id: + type: integer + format: int32 + readOnly: true + name: + type: string + nullable: true + actions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + nullable: true + _defaultAuthorizationStrategiesForCRUD: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimActionAuthStrategies' + nullable: true + readOnly: true + authorizationStrategyOverridesForCRUD: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimActionAuthStrategies' + nullable: true + children: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimModel' + description: Children are collection of ResourceClaim + nullable: true + additionalProperties: false + copyClaimSetRequest: + title: CopyClaimSetRequest + type: object + properties: + originalId: + type: integer + description: ClaimSet id to copy + format: int32 + name: + type: string + description: New claimset name + additionalProperties: false + editApplicationRequest: + title: EditApplicationRequest + type: object + properties: + applicationName: + type: string + description: Application name + vendorId: + type: integer + description: Vendor/ company id + format: int32 + claimSetName: + type: string + description: Claim set name + profileIds: + type: array + items: + type: integer + format: int32 + description: Profile id + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + description: Education organization ids + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + additionalProperties: false + editClaimSetRequest: + title: EditClaimSetRequest + type: object + properties: + name: + type: string + description: Claim set name + additionalProperties: false + editOdsInstanceContextRequest: + title: EditOdsInstanceContextRequest + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance context ODS instance id. + format: int32 + contextKey: + type: string + description: context key. + contextValue: + type: string + description: context value. + additionalProperties: false + editOdsInstanceDerivativeRequest: + title: EditOdsInstanceDerivativeRequest + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance derivative ODS instance id. + format: int32 + derivativeType: + type: string + description: derivative type. + connectionString: + type: string + description: connection string. + additionalProperties: false + editOdsInstanceRequest: + title: EditOdsInstanceRequest + type: object + properties: + name: + type: string + description: Ods Instance name + instanceType: + type: string + description: Ods Instance type + nullable: true + connectionString: + type: string + description: Ods Instance connection string + nullable: true + additionalProperties: false + editProfileRequest: + title: EditProfileRequest + type: object + properties: + name: + type: string + description: Profile name + definition: + type: string + description: Profile definition + additionalProperties: false + example: "{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + editResourceClaimOnClaimSetRequest: + title: EditResourceClaimActionsOnClaimSetRequest + type: object + properties: + resourceClaimActions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + additionalProperties: false + editVendorRequest: + title: EditVendorRequest + type: object + properties: + company: + type: string + description: Vendor/ company name + namespacePrefixes: + type: string + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + contactName: + type: string + description: Vendor contact name + contactEmailAddress: + type: string + description: Vendor contact email id + additionalProperties: false + importClaimSetRequest: + title: ImportClaimSetRequest + type: object + properties: + name: + type: string + description: Claim set name + resourceClaims: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimModel' + description: Resource Claims + additionalProperties: false + informationResult: + title: Information + type: object + properties: + version: + type: string + description: Application version + build: + type: string + description: Build / release version + additionalProperties: false + odsInstanceContextModel: + title: OdsInstanceContext + type: object + properties: + id: + type: integer + format: int32 + odsInstanceId: + type: integer + format: int32 + contextKey: + type: string + nullable: true + contextValue: + type: string + nullable: true + additionalProperties: false + odsInstanceDerivativeModel: + title: OdsInstanceDerivative + type: object + properties: + id: + type: integer + format: int32 + odsInstanceId: + type: integer + format: int32 + nullable: true + derivativeType: + type: string + nullable: true + additionalProperties: false + odsInstanceDetailModel: + title: OdsInstanceDetail + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + instanceType: + type: string + nullable: true + odsInstanceContexts: + type: array + items: + $ref: '#/components/schemas/odsInstanceContextModel' + nullable: true + odsInstanceDerivatives: + type: array + items: + $ref: '#/components/schemas/odsInstanceDerivativeModel' + nullable: true + additionalProperties: false + odsInstanceModel: + title: OdsInstance + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + instanceType: + type: string + nullable: true + additionalProperties: false + overrideAuthStategyOnClaimSetRequest: + title: OverrideAuthStategyOnClaimSetRequest + type: object + properties: + actionName: + type: string + nullable: true + authorizationStrategies: + type: array + items: + type: string + description: AuthorizationStrategy Names + additionalProperties: false + profileDetailsModel: + title: ProfileDetails + type: object + properties: + id: + type: integer + format: int32 + nullable: true + name: + type: string + nullable: true + definition: + type: string + nullable: true + additionalProperties: false + profileModel: + title: Profile + type: object + properties: + id: + type: integer + format: int32 + nullable: true + name: + type: string + nullable: true + additionalProperties: false + registerClientRequest: + title: RegisterClientRequest + type: object + properties: + clientId: + type: string + description: Client id + clientSecret: + type: string + description: Client secret + displayName: + type: string + description: Client display name + additionalProperties: false + resourceClaimAction: + title: ResourceClaimAction + type: object + properties: + name: + type: string + nullable: true + enabled: + type: boolean + additionalProperties: false + resourceClaimModel: + title: ResourceClaimModel + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + parentId: + type: integer + format: int32 + nullable: true + parentName: + type: string + nullable: true + children: + type: array + items: + $ref: '#/components/schemas/resourceClaimModel' + description: Children are collection of SimpleResourceClaimModel + nullable: true + additionalProperties: false + simpleApplicationModel: + title: Application + type: object + properties: + applicationName: + type: string + nullable: true + additionalProperties: false + vendorModel: + title: Vendor + type: object + properties: + id: + type: integer + format: int32 + nullable: true + company: + type: string + nullable: true + namespacePrefixes: + type: string + nullable: true + contactName: + type: string + nullable: true + contactEmailAddress: + type: string + nullable: true + additionalProperties: false + securitySchemes: + oauth: + type: oauth2 + flows: + clientCredentials: + tokenUrl: http://localhost/connect/token + scopes: + edfi_admin_api/full_access: Unrestricted access to all Admin API endpoints +security: + - oauth: + - api diff --git a/docs/api-specifications/openapi-yaml/admin-api-2.3.0-pre.yaml b/docs/api-specifications/openapi-yaml/admin-api-2.3.0-pre.yaml new file mode 100644 index 000000000..dbbbb84b6 --- /dev/null +++ b/docs/api-specifications/openapi-yaml/admin-api-2.3.0-pre.yaml @@ -0,0 +1,3053 @@ +openapi: 3.0.1 +info: + title: Admin API Documentation + description: 'The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API.' + version: v2 +paths: + /v2/resourceClaims: + get: + tags: + - ResourceClaims + summary: Retrieves all resourceClaims. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Resource Claim Id + schema: + type: integer + format: int32 + - name: name + in: query + description: Resource Claim Name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/resourceClaimModel' + '/v2/resourceClaims/{id}': + get: + tags: + - ResourceClaims + summary: Retrieves a specific resourceClaim based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/resourceClaimModel' + /v2/resourceClaimActions: + get: + tags: + - ResourceClaimActions + summary: Retrieves all resourceClaimActions. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: resourceName + in: query + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/resourceClaimActionModel' + /v2/resourceClaimActionAuthStrategies: + get: + tags: + - ResourceClaimActionAuthStrategies + summary: Retrieves all resourceClaimActionAuthStrategies. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: resourceName + in: query + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/resourceClaimActionAuthStrategyModel' + /v2/vendors: + get: + tags: + - Vendors + summary: Retrieves all vendors. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Vendor/ company id + schema: + type: integer + format: int32 + - name: company + in: query + description: Vendor/ company name + schema: + type: string + - name: namespacePrefixes + in: query + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + schema: + type: string + - name: contactName + in: query + description: Vendor contact name + schema: + type: string + - name: contactEmailAddress + in: query + description: Vendor contact email id + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/vendorModel' + post: + tags: + - Vendors + summary: Creates vendor based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addVendorRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/vendors/{id}': + get: + tags: + - Vendors + summary: Retrieves a specific vendor based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/vendorModel' + put: + tags: + - Vendors + summary: Updates vendor based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editVendorRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - Vendors + summary: Deletes an existing vendor using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/profiles: + get: + tags: + - Profiles + summary: Retrieves all profiles. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Profile id + schema: + type: integer + format: int32 + - name: name + in: query + description: Profile name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/profileModel' + post: + tags: + - Profiles + summary: Creates profile based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addProfileRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/profiles/{id}': + get: + tags: + - Profiles + summary: Retrieves a specific profile based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/profileDetailsModel' + put: + tags: + - Profiles + summary: Updates profile based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editProfileRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - Profiles + summary: Deletes an existing profile using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstances: + get: + tags: + - OdsInstances + summary: Retrieves all odsInstances. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: List of ODS instance id + schema: + type: integer + format: int32 + - name: name + in: query + description: Ods Instance name + schema: + type: string + - name: instanceType + in: query + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/odsInstanceModel' + post: + tags: + - OdsInstances + summary: Creates odsInstance based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsInstanceRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstances/{id}': + get: + tags: + - OdsInstances + summary: Retrieves a specific odsInstance based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceDetailModel' + put: + tags: + - OdsInstances + summary: Updates odsInstance based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - OdsInstances + summary: Deletes an existing odsInstance using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstanceDerivatives: + get: + tags: + - OdsInstanceDerivatives + summary: Retrieves all odsInstanceDerivatives. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/odsInstanceDerivativeModel' + post: + tags: + - OdsInstanceDerivatives + summary: Creates odsInstanceDerivative based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsInstanceDerivativeRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstanceDerivatives/{id}': + get: + tags: + - OdsInstanceDerivatives + summary: Retrieves a specific odsInstanceDerivative based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceDerivativeModel' + put: + tags: + - OdsInstanceDerivatives + summary: Updates odsInstanceDerivative based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceDerivativeRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - OdsInstanceDerivatives + summary: Deletes an existing odsInstanceDerivative using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstanceContexts: + get: + tags: + - OdsInstanceContexts + summary: Retrieves all odsInstanceContexts. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/odsInstanceContextModel' + post: + tags: + - OdsInstanceContexts + summary: Creates odsInstanceContext based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsInstanceContextRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstanceContexts/{id}': + get: + tags: + - OdsInstanceContexts + summary: Retrieves a specific odsInstanceContext based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceContextModel' + put: + tags: + - OdsInstanceContexts + summary: Updates odsInstanceContext based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceContextRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - OdsInstanceContexts + summary: Deletes an existing odsInstanceContext using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + '/v2/claimSets/{id}/export': + get: + tags: + - ClaimSets + summary: Exports a specific claimset by id + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/claimSetDetailsModel' + /v2/claimSets: + get: + tags: + - ClaimSets + summary: Retrieves all claimSets. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Claim set id + schema: + type: integer + format: int32 + - name: name + in: query + description: Claim set name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/claimSetModel' + post: + tags: + - ClaimSets + summary: Creates claimSet based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/claimSets/{id}': + get: + tags: + - ClaimSets + summary: Retrieves a specific claimSet based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/claimSetDetailsModel' + put: + tags: + - ClaimSets + summary: Updates claimSet based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - ClaimSets + summary: Deletes an existing claimSet using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/authorizationStrategies: + get: + tags: + - AuthorizationStrategies + summary: Retrieves all authorizationStrategies. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/authorizationStrategyModel' + /v2/applications: + get: + tags: + - Applications + summary: Retrieves all applications. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Application id + schema: + type: integer + format: int32 + - name: applicationName + in: query + description: Application name + schema: + type: string + - name: claimsetName + in: query + description: Claim set name + schema: + type: string + - name: ids + in: query + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/applicationModel' + post: + tags: + - Applications + summary: Creates application based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addApplicationRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/applicationResult' + '/v2/applications/{id}': + get: + tags: + - Applications + summary: Retrieves a specific application based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/applicationModel' + put: + tags: + - Applications + summary: Updates application based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editApplicationRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - Applications + summary: Deletes an existing application using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + '/v2/odsInstances/{id}/applications': + get: + tags: + - OdsInstances + summary: Retrieves applications assigned to a specific ODS instance based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/applicationModel' + '/v2/vendors/{id}/applications': + get: + tags: + - Vendors + summary: Retrieves applications assigned to a specific vendor based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/applicationModel' + /v2/apiclients: + get: + tags: + - Apiclients + summary: Retrieves all apiclients. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: applicationid + in: query + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/apiClientModel' + post: + tags: + - Apiclients + summary: Creates apiclient based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addApiClientRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/apiClientResult' + '/v2/apiclients/{id}': + get: + tags: + - Apiclients + summary: Retrieves a specific apiclient based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/apiClientModel' + put: + tags: + - Apiclients + summary: Updates apiclient based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editApiClientRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - Apiclients + summary: Deletes an existing apiclient using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/actions: + get: + tags: + - Actions + summary: Retrieves all actions. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Action id + schema: + type: integer + format: int32 + - name: name + in: query + description: Action name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/actionModel' + /: + get: + tags: + - Information + summary: Retrieve API informational metadata + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/informationResult' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + content: + application/json: + schema: + $ref: '#/components/schemas/informationResult' + /v2/claimSets/copy: + post: + tags: + - ClaimSets + summary: Copies the existing claimset and create a new one. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/copyClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + /v2/claimSets/import: + post: + tags: + - ClaimSets + summary: Imports a new claimset + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/importClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/overrideAuthorizationStrategy': + post: + tags: + - ClaimSets + summary: Overrides the default authorization strategies on provided resource claim for a specific action. + description: "Override the default authorization strategies on provided resource claim for a specific action.\r\n\r\nex: actionName = read, authorizationStrategies= [ \"Ownershipbased\" ]" + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/overrideAuthStategyOnClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/resetAuthorizationStrategies': + post: + tags: + - ClaimSets + summary: Resets to default authorization strategies on provided resource claim. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + '/v2/claimSets/{claimSetId}/resourceClaimActions': + post: + tags: + - ClaimSets + summary: Adds ResourceClaimAction association to a claim set. + description: "Add resourceClaimAction association to claim set. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges.\r\nresouceclaimId is required fields." + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addResourceClaimOnClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + /connect/register: + post: + tags: + - Connect + summary: Registers new client + description: Registers new client + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + ClientId: + type: string + description: Client id + ClientSecret: + type: string + description: Client secret + DisplayName: + type: string + description: Client display name + encoding: + ClientId: + style: form + ClientSecret: + style: form + DisplayName: + style: form + responses: + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Application registered successfully. + /connect/token: + post: + tags: + - Connect + summary: Retrieves bearer token + description: "\nTo authenticate Swagger requests, execute using \"Authorize\" above, not \"Try It Out\" here." + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + client_id: + type: 'string ' + client_secret: + type: 'string ' + grant_type: + type: 'string ' + scope: + type: string + responses: + '400': + description: 'Bad request, such as invalid scope.' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Sign-in successful. + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}': + put: + tags: + - ClaimSets + summary: Updates the ResourceClaimActions to a specific resource claim on a claimset. + description: 'Updates the resourceClaimActions to a specific resource claim on a claimset. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges.' + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editResourceClaimOnClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - ClaimSets + summary: Deletes a resource claims association from a claimset + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + '/v2/applications/{id}/reset-credential': + put: + tags: + - Applications + summary: Reset application credentials. Returns new key and secret. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/applicationResult' + '/v2/apiclients/{id}/reset-credential': + put: + tags: + - Apiclients + summary: Reset apiclient credentials. Returns new key and secret. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/apiClientResult' +components: + schemas: + actionForResourceClaimModel: + type: object + properties: + name: + type: string + nullable: true + additionalProperties: false + actionModel: + title: Action + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + uri: + type: string + nullable: true + additionalProperties: false + actionWithAuthorizationStrategy: + type: object + properties: + actionId: + type: integer + format: int32 + actionName: + type: string + nullable: true + authorizationStrategies: + type: array + items: + $ref: '#/components/schemas/authorizationStrategyModelForAction' + nullable: true + additionalProperties: false + addApiClientRequest: + title: AddApiClientRequest + type: object + properties: + name: + type: string + description: Api client name + isApproved: + type: boolean + description: Is approved + applicationId: + type: integer + description: Application id + format: int32 + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + additionalProperties: false + addApplicationRequest: + title: AddApplicationRequest + type: object + properties: + applicationName: + type: string + description: Application name + vendorId: + type: integer + description: Vendor/ company id + format: int32 + claimSetName: + type: string + description: Claim set name + profileIds: + type: array + items: + type: integer + format: int32 + description: Profile id + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + description: Education organization ids + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + enabled: + type: boolean + description: Indicates whether the ApiClient's credetials is enabled. Defaults to true if not provided. + nullable: true + additionalProperties: false + addClaimSetRequest: + title: AddClaimSetRequest + type: object + properties: + name: + type: string + description: Claim set name + additionalProperties: false + addOdsInstanceContextRequest: + title: AddOdsInstanceContextRequest + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance context ODS instance id. + format: int32 + contextKey: + type: string + description: context key. + contextValue: + type: string + description: context value. + additionalProperties: false + addOdsInstanceDerivativeRequest: + title: AddOdsInstanceDerivativeRequest + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance derivative ODS instance id. + format: int32 + derivativeType: + type: string + description: derivative type. + connectionString: + type: string + description: connection string. + additionalProperties: false + addOdsInstanceRequest: + title: AddOdsInstanceRequest + type: object + properties: + name: + type: string + description: Ods Instance name + instanceType: + type: string + description: Ods Instance type + nullable: true + connectionString: + type: string + description: Ods Instance connection string + additionalProperties: false + addProfileRequest: + title: AddProfileRequest + type: object + properties: + name: + type: string + description: Profile name + definition: + type: string + description: Profile definition + additionalProperties: false + example: "{\n \"name\": \"Test-Profile\",\n \"definition\": \"\"\n}" + addResourceClaimOnClaimSetRequest: + title: AddResourceClaimActionsOnClaimSetRequest + type: object + properties: + resourceClaimId: + type: integer + description: ResourceClaim id + format: int32 + resourceClaimActions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + additionalProperties: false + addVendorRequest: + title: AddVendorRequest + type: object + properties: + company: + type: string + description: Vendor/ company name + namespacePrefixes: + type: string + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + contactName: + type: string + description: Vendor contact name + contactEmailAddress: + type: string + description: Vendor contact email id + additionalProperties: false + adminApiError: + title: AdminApiError + type: object + additionalProperties: false + description: Wrapper schema for all error responses + apiClientModel: + title: ApiClient + type: object + properties: + id: + type: integer + format: int32 + key: + type: string + nullable: true + name: + type: string + nullable: true + isApproved: + type: boolean + useSandbox: + type: boolean + sandboxType: + type: integer + format: int32 + applicationId: + type: integer + format: int32 + keyStatus: + type: string + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + nullable: true + odsInstanceIds: + type: array + items: + type: integer + format: int32 + nullable: true + additionalProperties: false + apiClientResult: + title: ApiClient + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + key: + type: string + nullable: true + secret: + type: string + nullable: true + applicationId: + type: integer + format: int32 + additionalProperties: false + applicationModel: + title: Application + type: object + properties: + id: + type: integer + format: int32 + applicationName: + type: string + nullable: true + claimSetName: + type: string + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + nullable: true + vendorId: + type: integer + format: int32 + nullable: true + profileIds: + type: array + items: + type: integer + format: int32 + nullable: true + odsInstanceIds: + type: array + items: + type: integer + format: int32 + nullable: true + enabled: + type: boolean + additionalProperties: false + applicationResult: + title: ApplicationKeySecret + type: object + properties: + id: + type: integer + format: int32 + key: + type: string + nullable: true + secret: + type: string + nullable: true + additionalProperties: false + authorizationStrategy: + title: ResourceClaimAuthorizationStrategy + type: object + properties: + authStrategyId: + type: integer + format: int32 + authStrategyName: + type: string + nullable: true + isInheritedFromParent: + type: boolean + additionalProperties: false + authorizationStrategyModel: + title: AuthorizationStrategy + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + displayName: + type: string + nullable: true + additionalProperties: false + authorizationStrategyModelForAction: + type: object + properties: + authStrategyId: + type: integer + format: int32 + authStrategyName: + type: string + nullable: true + additionalProperties: false + claimSetDetailsModel: + title: ClaimSetWithResources + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + _isSystemReserved: + type: boolean + readOnly: true + _applications: + type: array + items: + $ref: '#/components/schemas/simpleApplicationModel' + nullable: true + readOnly: true + resourceClaims: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimModel' + nullable: true + additionalProperties: false + claimSetModel: + title: ClaimSet + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + _isSystemReserved: + type: boolean + readOnly: true + _applications: + type: array + items: + $ref: '#/components/schemas/simpleApplicationModel' + nullable: true + readOnly: true + additionalProperties: false + claimSetResourceClaimActionAuthStrategies: + title: ClaimSetResourceClaimActionAuthorizationStrategies + type: object + properties: + actionId: + type: integer + format: int32 + nullable: true + actionName: + type: string + nullable: true + authorizationStrategies: + type: array + items: + $ref: '#/components/schemas/authorizationStrategy' + nullable: true + additionalProperties: false + claimSetResourceClaimModel: + title: ClaimSetResourceClaim + type: object + properties: + id: + type: integer + format: int32 + readOnly: true + name: + type: string + nullable: true + actions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + nullable: true + _defaultAuthorizationStrategiesForCRUD: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimActionAuthStrategies' + nullable: true + readOnly: true + authorizationStrategyOverridesForCRUD: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimActionAuthStrategies' + nullable: true + children: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimModel' + description: Children are collection of ResourceClaim + nullable: true + additionalProperties: false + copyClaimSetRequest: + title: CopyClaimSetRequest + type: object + properties: + originalId: + type: integer + description: ClaimSet id to copy + format: int32 + name: + type: string + description: New claimset name + additionalProperties: false + editApiClientRequest: + title: EditApiClientRequest + type: object + properties: + name: + type: string + description: Api client name + isApproved: + type: boolean + description: Is approved + applicationId: + type: integer + description: Application id + format: int32 + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + additionalProperties: false + editApplicationRequest: + title: EditApplicationRequest + type: object + properties: + applicationName: + type: string + description: Application name + vendorId: + type: integer + description: Vendor/ company id + format: int32 + claimSetName: + type: string + description: Claim set name + profileIds: + type: array + items: + type: integer + format: int32 + description: Profile id + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + description: Education organization ids + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + enabled: + type: boolean + description: Indicates whether the ApiClient's credetials is enabled. Defaults to true if not provided. + nullable: true + additionalProperties: false + editClaimSetRequest: + title: EditClaimSetRequest + type: object + properties: + name: + type: string + description: Claim set name + additionalProperties: false + editOdsInstanceContextRequest: + title: EditOdsInstanceContextRequest + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance context ODS instance id. + format: int32 + contextKey: + type: string + description: context key. + contextValue: + type: string + description: context value. + additionalProperties: false + editOdsInstanceDerivativeRequest: + title: EditOdsInstanceDerivativeRequest + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance derivative ODS instance id. + format: int32 + derivativeType: + type: string + description: derivative type. + connectionString: + type: string + description: connection string. + additionalProperties: false + editOdsInstanceRequest: + title: EditOdsInstanceRequest + type: object + properties: + name: + type: string + description: Ods Instance name + instanceType: + type: string + description: Ods Instance type + nullable: true + connectionString: + type: string + description: Ods Instance connection string + nullable: true + additionalProperties: false + editProfileRequest: + title: EditProfileRequest + type: object + properties: + name: + type: string + description: Profile name + definition: + type: string + description: Profile definition + additionalProperties: false + example: "{\n \"name\": \"Test-Profile\",\n \"definition\": \"\"\n}" + editResourceClaimOnClaimSetRequest: + title: EditResourceClaimActionsOnClaimSetRequest + type: object + properties: + resourceClaimActions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + additionalProperties: false + editVendorRequest: + title: EditVendorRequest + type: object + properties: + company: + type: string + description: Vendor/ company name + namespacePrefixes: + type: string + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + contactName: + type: string + description: Vendor contact name + contactEmailAddress: + type: string + description: Vendor contact email id + additionalProperties: false + importClaimSetRequest: + title: ImportClaimSetRequest + type: object + properties: + name: + type: string + description: Claim set name + resourceClaims: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimModel' + description: Resource Claims + additionalProperties: false + informationResult: + title: Information + type: object + properties: + version: + type: string + description: Application version + build: + type: string + description: Build / release version + additionalProperties: false + odsInstanceContextModel: + title: OdsInstanceContext + type: object + properties: + id: + type: integer + format: int32 + odsInstanceId: + type: integer + format: int32 + contextKey: + type: string + nullable: true + contextValue: + type: string + nullable: true + additionalProperties: false + odsInstanceDerivativeModel: + title: OdsInstanceDerivative + type: object + properties: + id: + type: integer + format: int32 + odsInstanceId: + type: integer + format: int32 + nullable: true + derivativeType: + type: string + nullable: true + additionalProperties: false + odsInstanceDetailModel: + title: OdsInstanceDetail + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + instanceType: + type: string + nullable: true + odsInstanceContexts: + type: array + items: + $ref: '#/components/schemas/odsInstanceContextModel' + nullable: true + odsInstanceDerivatives: + type: array + items: + $ref: '#/components/schemas/odsInstanceDerivativeModel' + nullable: true + additionalProperties: false + odsInstanceModel: + title: OdsInstance + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + instanceType: + type: string + nullable: true + additionalProperties: false + overrideAuthStategyOnClaimSetRequest: + title: OverrideAuthStategyOnClaimSetRequest + type: object + properties: + actionName: + type: string + nullable: true + authorizationStrategies: + type: array + items: + type: string + description: AuthorizationStrategy Names + additionalProperties: false + profileDetailsModel: + title: ProfileDetails + type: object + properties: + id: + type: integer + format: int32 + nullable: true + name: + type: string + nullable: true + definition: + type: string + nullable: true + additionalProperties: false + profileModel: + title: Profile + type: object + properties: + id: + type: integer + format: int32 + nullable: true + name: + type: string + nullable: true + additionalProperties: false + registerClientRequest: + title: RegisterClientRequest + type: object + properties: + clientId: + type: string + description: Client id + clientSecret: + type: string + description: Client secret + displayName: + type: string + description: Client display name + additionalProperties: false + resourceClaimAction: + title: ResourceClaimAction + type: object + properties: + name: + type: string + nullable: true + enabled: + type: boolean + additionalProperties: false + resourceClaimActionAuthStrategyModel: + type: object + properties: + resourceClaimId: + type: integer + format: int32 + resourceName: + type: string + nullable: true + claimName: + type: string + nullable: true + authorizationStrategiesForActions: + type: array + items: + $ref: '#/components/schemas/actionWithAuthorizationStrategy' + nullable: true + additionalProperties: false + resourceClaimActionModel: + type: object + properties: + resourceClaimId: + type: integer + format: int32 + resourceName: + type: string + nullable: true + claimName: + type: string + nullable: true + actions: + type: array + items: + $ref: '#/components/schemas/actionForResourceClaimModel' + nullable: true + additionalProperties: false + resourceClaimModel: + title: ResourceClaimModel + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + parentId: + type: integer + format: int32 + nullable: true + parentName: + type: string + nullable: true + children: + type: array + items: + $ref: '#/components/schemas/resourceClaimModel' + description: Children are collection of SimpleResourceClaimModel + nullable: true + additionalProperties: false + simpleApplicationModel: + title: Application + type: object + properties: + applicationName: + type: string + nullable: true + additionalProperties: false + vendorModel: + title: Vendor + type: object + properties: + id: + type: integer + format: int32 + nullable: true + company: + type: string + nullable: true + namespacePrefixes: + type: string + nullable: true + contactName: + type: string + nullable: true + contactEmailAddress: + type: string + nullable: true + additionalProperties: false + securitySchemes: + oauth: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://localhost/connect/token + scopes: + edfi_admin_api/full_access: Full access to the Admin API + edfi_admin_api/tenant_access: Access to a specific tenant + edfi_admin_api/worker: Worker access to the Admin API +security: + - oauth: + - api \ No newline at end of file diff --git a/docs/api-specifications/openapi-yaml/admin-api-2.3.0.yaml b/docs/api-specifications/openapi-yaml/admin-api-2.3.0.yaml new file mode 100644 index 000000000..16841d7e1 --- /dev/null +++ b/docs/api-specifications/openapi-yaml/admin-api-2.3.0.yaml @@ -0,0 +1,2965 @@ +openapi: 3.0.1 +info: + title: Admin API Documentation + description: 'The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API.' + version: v2 +paths: + /v2/tenants: + get: + tags: + - Tenants + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '/v2/tenants/{tenantName}': + get: + tags: + - Tenants + parameters: + - name: tenantName + in: path + required: true + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + /v2/resourceClaims: + get: + tags: + - ResourceClaims + summary: Retrieves all resourceClaims. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Resource Claim Id + schema: + type: integer + format: int32 + - name: name + in: query + description: Resource Claim Name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/resourceClaimModel' + '/v2/resourceClaims/{id}': + get: + tags: + - ResourceClaims + summary: Retrieves a specific resourceClaim based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/resourceClaimModel' + /v2/resourceClaimActions: + get: + tags: + - ResourceClaimActions + summary: Retrieves all resourceClaimActions. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: resourceName + in: query + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/resourceClaimActionModel' + /v2/resourceClaimActionAuthStrategies: + get: + tags: + - ResourceClaimActionAuthStrategies + summary: Retrieves all resourceClaimActionAuthStrategies. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: resourceName + in: query + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/resourceClaimActionAuthStrategyModel' + /v2/vendors: + get: + tags: + - Vendors + summary: Retrieves all vendors. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Vendor/ company id + schema: + type: integer + format: int32 + - name: company + in: query + description: Vendor/ company name + schema: + type: string + - name: namespacePrefixes + in: query + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + schema: + type: string + - name: contactName + in: query + description: Vendor contact name + schema: + type: string + - name: contactEmailAddress + in: query + description: Vendor contact email id + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/vendorModel' + post: + tags: + - Vendors + summary: Creates vendor based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addVendorRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/vendors/{id}': + get: + tags: + - Vendors + summary: Retrieves a specific vendor based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/vendorModel' + put: + tags: + - Vendors + summary: Updates vendor based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editVendorRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - Vendors + summary: Deletes an existing vendor using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/profiles: + get: + tags: + - Profiles + summary: Retrieves all profiles. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Profile id + schema: + type: integer + format: int32 + - name: name + in: query + description: Profile name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/profileModel' + post: + tags: + - Profiles + summary: Creates profile based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addProfileRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/profiles/{id}': + get: + tags: + - Profiles + summary: Retrieves a specific profile based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/profileDetailsModel' + put: + tags: + - Profiles + summary: Updates profile based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editProfileRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - Profiles + summary: Deletes an existing profile using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstances: + get: + tags: + - OdsInstances + summary: Retrieves all odsInstances. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: List of ODS instance id + schema: + type: integer + format: int32 + - name: name + in: query + description: Ods Instance name + schema: + type: string + - name: instanceType + in: query + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/odsInstanceModel' + post: + tags: + - OdsInstances + summary: Creates odsInstance based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsInstanceRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstances/{id}': + get: + tags: + - OdsInstances + summary: Retrieves a specific odsInstance based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceDetailModel' + put: + tags: + - OdsInstances + summary: Updates odsInstance based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - OdsInstances + summary: Deletes an existing odsInstance using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstanceDerivatives: + get: + tags: + - OdsInstanceDerivatives + summary: Retrieves all odsInstanceDerivatives. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/odsInstanceDerivativeModel' + post: + tags: + - OdsInstanceDerivatives + summary: Creates odsInstanceDerivative based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsInstanceDerivativeRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstanceDerivatives/{id}': + get: + tags: + - OdsInstanceDerivatives + summary: Retrieves a specific odsInstanceDerivative based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceDerivativeModel' + put: + tags: + - OdsInstanceDerivatives + summary: Updates odsInstanceDerivative based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceDerivativeRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - OdsInstanceDerivatives + summary: Deletes an existing odsInstanceDerivative using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstanceContexts: + get: + tags: + - OdsInstanceContexts + summary: Retrieves all odsInstanceContexts. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/odsInstanceContextModel' + post: + tags: + - OdsInstanceContexts + summary: Creates odsInstanceContext based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addOdsInstanceContextRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstanceContexts/{id}': + get: + tags: + - OdsInstanceContexts + summary: Retrieves a specific odsInstanceContext based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/odsInstanceContextModel' + put: + tags: + - OdsInstanceContexts + summary: Updates odsInstanceContext based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editOdsInstanceContextRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - OdsInstanceContexts + summary: Deletes an existing odsInstanceContext using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + '/v2/claimSets/{id}/export': + get: + tags: + - ClaimSets + summary: Exports a specific claimset by id + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/claimSetDetailsModel' + /v2/claimSets: + get: + tags: + - ClaimSets + summary: Retrieves all claimSets. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Claim set id + schema: + type: integer + format: int32 + - name: name + in: query + description: Claim set name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/claimSetModel' + post: + tags: + - ClaimSets + summary: Creates claimSet based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/claimSets/{id}': + get: + tags: + - ClaimSets + summary: Retrieves a specific claimSet based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/claimSetDetailsModel' + put: + tags: + - ClaimSets + summary: Updates claimSet based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - ClaimSets + summary: Deletes an existing claimSet using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/authorizationStrategies: + get: + tags: + - AuthorizationStrategies + summary: Retrieves all authorizationStrategies. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/authorizationStrategyModel' + /v2/applications: + get: + tags: + - Applications + summary: Retrieves all applications. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Application id + schema: + type: integer + format: int32 + - name: applicationName + in: query + description: Application name + schema: + type: string + - name: claimsetName + in: query + description: Claim set name + schema: + type: string + - name: ids + in: query + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/applicationModel' + post: + tags: + - Applications + summary: Creates application based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addApplicationRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/applicationResult' + '/v2/applications/{id}': + get: + tags: + - Applications + summary: Retrieves a specific application based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/applicationModel' + put: + tags: + - Applications + summary: Updates application based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editApplicationRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - Applications + summary: Deletes an existing application using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + '/v2/odsInstances/{id}/applications': + get: + tags: + - OdsInstances + summary: Retrieves applications assigned to a specific ODS instance based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/applicationModel' + '/v2/vendors/{id}/applications': + get: + tags: + - Vendors + summary: Retrieves applications assigned to a specific vendor based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/applicationModel' + /v2/apiclients: + get: + tags: + - Apiclients + summary: Retrieves all apiclients. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: applicationid + in: query + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/apiClientModel' + post: + tags: + - Apiclients + summary: Creates apiclient based on the supplied values. + description: 'The POST operation can be used to create or update resources. In database terms, this is often referred to as an "upsert" operation (insert + update). Clients should NOT include the resource "id" in the JSON body because it will result in an error. The web service will identify whether the resource already exists based on the natural key values provided, and update or create the resource appropriately. It is recommended to use POST for both create and update except while updating natural key of a resource in which case PUT operation must be used.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addApiClientRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/apiClientResult' + '/v2/apiclients/{id}': + get: + tags: + - Apiclients + summary: Retrieves a specific apiclient based on the identifier. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/apiClientModel' + put: + tags: + - Apiclients + summary: Updates apiclient based on the resource identifier. + description: 'The PUT operation is used to update a resource by identifier. If the resource identifier ("id") is provided in the JSON body, it will be ignored. Additionally, this API resource is not configured for cascading natural key updates. Natural key values for this resource cannot be changed using PUT operation, so the recommendation is to use POST as that supports upsert behavior.' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editApiClientRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - Apiclients + summary: Deletes an existing apiclient using the resource identifier. + description: 'The DELETE operation is used to delete an existing resource by identifier. If the resource doesn''t exist, an error will result (the resource will not be found).' + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/actions: + get: + tags: + - Actions + summary: Retrieves all actions. + description: This GET operation provides access to resources using the "Get" search pattern. The values of any properties of the resource that are specified will be used to return all matching results (if it exists). + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Action id + schema: + type: integer + format: int32 + - name: name + in: query + description: Action name + schema: + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/actionModel' + /: + get: + tags: + - Information + summary: Retrieve API informational metadata + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/informationResult' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + content: + application/json: + schema: + $ref: '#/components/schemas/informationResult' + /v2/claimSets/copy: + post: + tags: + - ClaimSets + summary: Copies the existing claimset and create a new one. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/copyClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + /v2/claimSets/import: + post: + tags: + - ClaimSets + summary: Imports a new claimset + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/importClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/overrideAuthorizationStrategy': + post: + tags: + - ClaimSets + summary: Overrides the default authorization strategies on provided resource claim for a specific action. + description: "Override the default authorization strategies on provided resource claim for a specific action.\r\n\r\nex: actionName = read, authorizationStrategies= [ \"Ownershipbased\" ]" + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/overrideAuthStategyOnClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/resetAuthorizationStrategies': + post: + tags: + - ClaimSets + summary: Resets to default authorization strategies on provided resource claim. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + '/v2/claimSets/{claimSetId}/resourceClaimActions': + post: + tags: + - ClaimSets + summary: Adds ResourceClaimAction association to a claim set. + description: "Add resourceClaimAction association to claim set. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges.\r\nresouceclaimId is required fields." + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/addResourceClaimOnClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + /connect/register: + post: + tags: + - Connect + summary: Registers new client + description: Registers new client + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + ClientId: + type: string + description: Client id + ClientSecret: + type: string + description: Client secret + DisplayName: + type: string + description: Client display name + encoding: + ClientId: + style: form + ClientSecret: + style: form + DisplayName: + style: form + responses: + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Application registered successfully. + /connect/token: + post: + tags: + - Connect + summary: Retrieves bearer token + description: "\nTo authenticate Swagger requests, execute using \"Authorize\" above, not \"Try It Out\" here." + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + client_id: + type: string + client_secret: + type: string + grant_type: + type: string + scope: + type: string + responses: + '400': + description: 'Bad request, such as invalid scope.' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Sign-in successful. + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}': + put: + tags: + - ClaimSets + summary: Updates the ResourceClaimActions to a specific resource claim on a claimset. + description: 'Updates the resourceClaimActions to a specific resource claim on a claimset. At least one action should be enabled. Valid actions are read, create, update, delete, readchanges.' + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/editResourceClaimOnClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + delete: + tags: + - ClaimSets + summary: Deletes a resource claims association from a claimset + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: OK + '/v2/applications/{id}/reset-credential': + put: + tags: + - Applications + summary: Reset application credentials. Returns new key and secret. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/applicationResult' + '/v2/apiclients/{id}/reset-credential': + put: + tags: + - Apiclients + summary: Reset apiclient credentials. Returns new key and secret. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/apiClientResult' +components: + schemas: + actionForResourceClaimModel: + type: object + properties: + name: + type: string + nullable: true + additionalProperties: false + actionModel: + title: Action + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + uri: + type: string + nullable: true + additionalProperties: false + actionWithAuthorizationStrategy: + type: object + properties: + actionId: + type: integer + format: int32 + actionName: + type: string + nullable: true + authorizationStrategies: + type: array + items: + $ref: '#/components/schemas/authorizationStrategyModelForAction' + nullable: true + additionalProperties: false + addApiClientRequest: + title: AddApiClientRequest + type: object + properties: + name: + type: string + description: Api client name + isApproved: + type: boolean + description: Is approved + applicationId: + type: integer + description: Application id + format: int32 + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + additionalProperties: false + addApplicationRequest: + title: AddApplicationRequest + type: object + properties: + applicationName: + type: string + description: Application name + vendorId: + type: integer + description: Vendor/ company id + format: int32 + claimSetName: + type: string + description: Claim set name + profileIds: + type: array + items: + type: integer + format: int32 + description: Profile id + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + description: Education organization ids + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + enabled: + type: boolean + description: Indicates whether the ApiClient's credetials is enabled. Defaults to true if not provided. + nullable: true + additionalProperties: false + addClaimSetRequest: + title: AddClaimSetRequest + type: object + properties: + name: + type: string + description: Claim set name + additionalProperties: false + addOdsInstanceContextRequest: + title: AddOdsInstanceContextRequest + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance context ODS instance id. + format: int32 + contextKey: + type: string + description: context key. + contextValue: + type: string + description: context value. + additionalProperties: false + addOdsInstanceDerivativeRequest: + title: AddOdsInstanceDerivativeRequest + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance derivative ODS instance id. + format: int32 + derivativeType: + type: string + description: derivative type. + connectionString: + type: string + description: connection string. + additionalProperties: false + addOdsInstanceRequest: + title: AddOdsInstanceRequest + type: object + properties: + name: + type: string + description: Ods Instance name + instanceType: + type: string + description: Ods Instance type + nullable: true + connectionString: + type: string + description: Ods Instance connection string + additionalProperties: false + addProfileRequest: + title: AddProfileRequest + type: object + properties: + name: + type: string + description: Profile name + definition: + type: string + description: Profile definition + additionalProperties: false + example: "{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + addResourceClaimOnClaimSetRequest: + title: AddResourceClaimActionsOnClaimSetRequest + type: object + properties: + resourceClaimId: + type: integer + description: ResourceClaim id + format: int32 + resourceClaimActions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + additionalProperties: false + addVendorRequest: + title: AddVendorRequest + type: object + properties: + company: + type: string + description: Vendor/ company name + namespacePrefixes: + type: string + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + contactName: + type: string + description: Vendor contact name + contactEmailAddress: + type: string + description: Vendor contact email id + additionalProperties: false + adminApiError: + title: AdminApiError + type: object + additionalProperties: false + description: Wrapper schema for all error responses + apiClientModel: + title: ApiClient + type: object + properties: + id: + type: integer + format: int32 + key: + type: string + nullable: true + name: + type: string + nullable: true + isApproved: + type: boolean + useSandbox: + type: boolean + sandboxType: + type: integer + format: int32 + applicationId: + type: integer + format: int32 + keyStatus: + type: string + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + nullable: true + odsInstanceIds: + type: array + items: + type: integer + format: int32 + nullable: true + additionalProperties: false + apiClientResult: + title: ApiClient + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + key: + type: string + nullable: true + secret: + type: string + nullable: true + applicationId: + type: integer + format: int32 + additionalProperties: false + applicationModel: + title: Application + type: object + properties: + id: + type: integer + format: int32 + applicationName: + type: string + nullable: true + claimSetName: + type: string + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + nullable: true + vendorId: + type: integer + format: int32 + nullable: true + profileIds: + type: array + items: + type: integer + format: int32 + nullable: true + odsInstanceIds: + type: array + items: + type: integer + format: int32 + nullable: true + enabled: + type: boolean + additionalProperties: false + applicationResult: + title: ApplicationKeySecret + type: object + properties: + id: + type: integer + format: int32 + key: + type: string + nullable: true + secret: + type: string + nullable: true + additionalProperties: false + authorizationStrategy: + title: ResourceClaimAuthorizationStrategy + type: object + properties: + authStrategyId: + type: integer + format: int32 + authStrategyName: + type: string + nullable: true + isInheritedFromParent: + type: boolean + additionalProperties: false + authorizationStrategyModel: + title: AuthorizationStrategy + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + displayName: + type: string + nullable: true + additionalProperties: false + authorizationStrategyModelForAction: + type: object + properties: + authStrategyId: + type: integer + format: int32 + authStrategyName: + type: string + nullable: true + additionalProperties: false + claimSetDetailsModel: + title: ClaimSetWithResources + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + _isSystemReserved: + type: boolean + readOnly: true + _applications: + type: array + items: + $ref: '#/components/schemas/simpleApplicationModel' + nullable: true + readOnly: true + resourceClaims: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimModel' + nullable: true + additionalProperties: false + claimSetModel: + title: ClaimSet + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + _isSystemReserved: + type: boolean + readOnly: true + _applications: + type: array + items: + $ref: '#/components/schemas/simpleApplicationModel' + nullable: true + readOnly: true + additionalProperties: false + claimSetResourceClaimActionAuthStrategies: + title: ClaimSetResourceClaimActionAuthorizationStrategies + type: object + properties: + actionId: + type: integer + format: int32 + nullable: true + actionName: + type: string + nullable: true + authorizationStrategies: + type: array + items: + $ref: '#/components/schemas/authorizationStrategy' + nullable: true + additionalProperties: false + claimSetResourceClaimModel: + title: ClaimSetResourceClaim + type: object + properties: + id: + type: integer + format: int32 + readOnly: true + name: + type: string + nullable: true + actions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + nullable: true + _defaultAuthorizationStrategiesForCRUD: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimActionAuthStrategies' + nullable: true + readOnly: true + authorizationStrategyOverridesForCRUD: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimActionAuthStrategies' + nullable: true + children: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimModel' + description: Children are collection of ResourceClaim + nullable: true + additionalProperties: false + copyClaimSetRequest: + title: CopyClaimSetRequest + type: object + properties: + originalId: + type: integer + description: ClaimSet id to copy + format: int32 + name: + type: string + description: New claimset name + additionalProperties: false + editApiClientRequest: + title: EditApiClientRequest + type: object + properties: + name: + type: string + description: Api client name + isApproved: + type: boolean + description: Is approved + applicationId: + type: integer + description: Application id + format: int32 + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + additionalProperties: false + editApplicationRequest: + title: EditApplicationRequest + type: object + properties: + applicationName: + type: string + description: Application name + vendorId: + type: integer + description: Vendor/ company id + format: int32 + claimSetName: + type: string + description: Claim set name + profileIds: + type: array + items: + type: integer + format: int32 + description: Profile id + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + description: Education organization ids + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + enabled: + type: boolean + description: Indicates whether the ApiClient's credetials is enabled. Defaults to true if not provided. + nullable: true + additionalProperties: false + editClaimSetRequest: + title: EditClaimSetRequest + type: object + properties: + name: + type: string + description: Claim set name + additionalProperties: false + editOdsInstanceContextRequest: + title: EditOdsInstanceContextRequest + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance context ODS instance id. + format: int32 + contextKey: + type: string + description: context key. + contextValue: + type: string + description: context value. + additionalProperties: false + editOdsInstanceDerivativeRequest: + title: EditOdsInstanceDerivativeRequest + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance derivative ODS instance id. + format: int32 + derivativeType: + type: string + description: derivative type. + connectionString: + type: string + description: connection string. + additionalProperties: false + editOdsInstanceRequest: + title: EditOdsInstanceRequest + type: object + properties: + name: + type: string + description: Ods Instance name + instanceType: + type: string + description: Ods Instance type + nullable: true + connectionString: + type: string + description: Ods Instance connection string + nullable: true + additionalProperties: false + editProfileRequest: + title: EditProfileRequest + type: object + properties: + name: + type: string + description: Profile name + definition: + type: string + description: Profile definition + additionalProperties: false + example: "{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + editResourceClaimOnClaimSetRequest: + title: EditResourceClaimActionsOnClaimSetRequest + type: object + properties: + resourceClaimActions: + type: array + items: + $ref: '#/components/schemas/resourceClaimAction' + additionalProperties: false + editVendorRequest: + title: EditVendorRequest + type: object + properties: + company: + type: string + description: Vendor/ company name + namespacePrefixes: + type: string + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + contactName: + type: string + description: Vendor contact name + contactEmailAddress: + type: string + description: Vendor contact email id + additionalProperties: false + importClaimSetRequest: + title: ImportClaimSetRequest + type: object + properties: + name: + type: string + description: Claim set name + resourceClaims: + type: array + items: + $ref: '#/components/schemas/claimSetResourceClaimModel' + description: Resource Claims + additionalProperties: false + informationResult: + title: Information + type: object + properties: + version: + type: string + description: Application version + build: + type: string + description: Build / release version + additionalProperties: false + odsInstanceContextModel: + title: OdsInstanceContext + type: object + properties: + id: + type: integer + format: int32 + odsInstanceId: + type: integer + format: int32 + contextKey: + type: string + nullable: true + contextValue: + type: string + nullable: true + additionalProperties: false + odsInstanceDerivativeModel: + title: OdsInstanceDerivative + type: object + properties: + id: + type: integer + format: int32 + odsInstanceId: + type: integer + format: int32 + nullable: true + derivativeType: + type: string + nullable: true + additionalProperties: false + odsInstanceDetailModel: + title: OdsInstanceDetail + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + instanceType: + type: string + nullable: true + odsInstanceContexts: + type: array + items: + $ref: '#/components/schemas/odsInstanceContextModel' + nullable: true + odsInstanceDerivatives: + type: array + items: + $ref: '#/components/schemas/odsInstanceDerivativeModel' + nullable: true + additionalProperties: false + odsInstanceModel: + title: OdsInstance + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + instanceType: + type: string + nullable: true + additionalProperties: false + overrideAuthStategyOnClaimSetRequest: + title: OverrideAuthStategyOnClaimSetRequest + type: object + properties: + actionName: + type: string + nullable: true + authorizationStrategies: + type: array + items: + type: string + description: AuthorizationStrategy Names + additionalProperties: false + profileDetailsModel: + title: ProfileDetails + type: object + properties: + id: + type: integer + format: int32 + nullable: true + name: + type: string + nullable: true + definition: + type: string + nullable: true + additionalProperties: false + profileModel: + title: Profile + type: object + properties: + id: + type: integer + format: int32 + nullable: true + name: + type: string + nullable: true + additionalProperties: false + registerClientRequest: + title: RegisterClientRequest + type: object + properties: + clientId: + type: string + description: Client id + clientSecret: + type: string + description: Client secret + displayName: + type: string + description: Client display name + additionalProperties: false + resourceClaimAction: + title: ResourceClaimAction + type: object + properties: + name: + type: string + nullable: true + enabled: + type: boolean + additionalProperties: false + resourceClaimActionAuthStrategyModel: + type: object + properties: + resourceClaimId: + type: integer + format: int32 + resourceName: + type: string + nullable: true + claimName: + type: string + nullable: true + authorizationStrategiesForActions: + type: array + items: + $ref: '#/components/schemas/actionWithAuthorizationStrategy' + nullable: true + additionalProperties: false + resourceClaimActionModel: + type: object + properties: + resourceClaimId: + type: integer + format: int32 + resourceName: + type: string + nullable: true + claimName: + type: string + nullable: true + actions: + type: array + items: + $ref: '#/components/schemas/actionForResourceClaimModel' + nullable: true + additionalProperties: false + resourceClaimModel: + title: ResourceClaimModel + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + nullable: true + parentId: + type: integer + format: int32 + nullable: true + parentName: + type: string + nullable: true + children: + type: array + items: + $ref: '#/components/schemas/resourceClaimModel' + description: Children are collection of SimpleResourceClaimModel + nullable: true + additionalProperties: false + simpleApplicationModel: + title: Application + type: object + properties: + applicationName: + type: string + nullable: true + additionalProperties: false + vendorModel: + title: Vendor + type: object + properties: + id: + type: integer + format: int32 + nullable: true + company: + type: string + nullable: true + namespacePrefixes: + type: string + nullable: true + contactName: + type: string + nullable: true + contactEmailAddress: + type: string + nullable: true + additionalProperties: false + securitySchemes: + oauth: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://localhost/connect/token + scopes: + edfi_admin_api/full_access: Full access to the Admin API +security: + - oauth: + - api \ No newline at end of file diff --git a/docs/api-specifications/openapi-yaml/admin-api-console-2.3.0-pre.yaml b/docs/api-specifications/openapi-yaml/admin-api-console-2.3.0-pre.yaml new file mode 100644 index 000000000..702feb76a --- /dev/null +++ b/docs/api-specifications/openapi-yaml/admin-api-console-2.3.0-pre.yaml @@ -0,0 +1,131 @@ +openapi: 3.0.1 +info: + title: Admin API Documentation + description: 'The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API.' + version: adminconsole +paths: + /: + get: + tags: + - Information + summary: Retrieve API informational metadata + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/informationResult' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + content: + application/json: + schema: + $ref: '#/components/schemas/informationResult' + /connect/register: + post: + tags: + - Connect + summary: Registers new client + description: Registers new client + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + ClientId: + type: string + description: Client id + ClientSecret: + type: string + description: Client secret + DisplayName: + type: string + description: Client display name + encoding: + ClientId: + style: form + ClientSecret: + style: form + DisplayName: + style: form + responses: + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Application registered successfully. + /connect/token: + post: + tags: + - Connect + summary: Retrieves bearer token + description: "\nTo authenticate Swagger requests, execute using \"Authorize\" above, not \"Try It Out\" here." + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + client_id: + type: 'string ' + client_secret: + type: 'string ' + grant_type: + type: 'string ' + scope: + type: string + responses: + '400': + description: 'Bad request, such as invalid scope.' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Sign-in successful. +components: + schemas: + adminApiError: + title: AdminApiError + type: object + additionalProperties: false + description: Wrapper schema for all error responses + informationResult: + title: Information + type: object + properties: + version: + type: string + description: Application version + build: + type: string + description: Build / release version + additionalProperties: false + registerClientRequest: + title: RegisterClientRequest + type: object + properties: + clientId: + type: string + description: Client id + clientSecret: + type: string + description: Client secret + displayName: + type: string + description: Client display name + additionalProperties: false + securitySchemes: + oauth: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://localhost/connect/token + scopes: + edfi_admin_api/full_access: Full access to the Admin API + edfi_admin_api/tenant_access: Access to a specific tenant + edfi_admin_api/worker: Worker access to the Admin API +security: + - oauth: + - api \ No newline at end of file diff --git a/docs/design/ADMINAPI-1284-adminconsole-definition-removal.md b/docs/design/ADMINAPI-1284-adminconsole-definition-removal.md new file mode 100644 index 000000000..86a56c10e --- /dev/null +++ b/docs/design/ADMINAPI-1284-adminconsole-definition-removal.md @@ -0,0 +1,273 @@ +# AdminConsole Endpoints Removal Analysis and Design + +## Overview + +This document outlines the findings from the analysis phase (Phase 1) of the ticket ADM### Proposed Endpoint Structure + +### New Endpoints + +| Current Endpoint | Proposed Endpoint | Purpose | +|------------------|-------------------|---------| +| `/adminconsole/tenants` | `/v2/tenants` | List all tenants | +| `/adminconsole/tenants/{tenantId}` | `/v2/tenants/{tenantId}` | Get tenant details | +| `/adminconsole/odsInstances/{id}` | `/v2/odsInstances/{id}/metadata` | Get instance details | + +## Current State Analysis + +### Existing Endpoints + +The following `/adminconsole` endpoints have been identified in the codebase: + +| Endpoint | HTTP Method | Purpose | +|----------|-------------|---------| +| `/tenants` | GET | List all tenants | +| `/tenants/{tenantId}` | GET | Get tenant details by ID | +| `/odsInstances` | GET | List all ODS instances | +| `/odsInstances/{id}` | GET | Get instance details by ID | +| `/instances` | GET | List all instances for worker use | +| `/instances/{id}` | GET | Get instance details for worker use | +| `/instances` | POST | Create a new instance | +| `/instances` | PUT | Update an existing instance | +| `/instances` | DELETE | Delete an instance | +| `/instances/{instanceId}/completed` | POST | Mark instance creation as completed | +| `/instances/{instanceId}/deletefailed` | POST | Mark instance deletion as failed | +| `/instances/{instanceId}/renameFailed` | POST | Mark instance rename as failed | +| `/instances/{instanceId}/renamed` | POST | Mark instance rename as completed | +| `/instances/{instanceId}/deleted` | POST | Mark instance deletion as completed | +| `/healthcheck` | GET | Get health check information | +| `/healthcheck` | POST | Create a health check entry | + +### Database Schema + +The admin console functionality currently uses the following tables in the `adminconsole` schema: + +1. `adminconsole.Instances` - Stores ODS instance information + * Maps to the `Instance` entity model + * Contains information about ODS instances including status, credentials, and metadata + * References `dbo.OdsInstances` via the `OdsInstanceId` foreign key + +2. `adminconsole.HealthChecks` - Stores health check information + * Maps to the `HealthCheck` entity model + * Used by the health check worker to track instance health + +3. `adminconsole.OdsInstanceContexts` - Stores context information for ODS instances + * Maps to the `OdsInstanceContext` entity model + * Contains context key-value pairs for ODS instances + +4. `adminconsole.OdsInstanceDerivatives` - Stores derivative information for ODS instances + * Maps to the `OdsInstanceDerivative` entity model + * Contains derivative type information for ODS instances + +### Code Dependencies + +#### Core Services + +1. `IAdminConsoleTenantsService` - Manages tenant operations + * Responsible for initializing and retrieving tenant information + * Used by the tenant endpoints + +2. `IAdminConsoleInstancesService` - Manages instance operations + * Responsible for initializing instance data + * Maps ODS instances to admin console instances + +3. `IAdminConsoleInitializationService` - Handles initialization of admin console data + * Initializes applications needed for the admin console + +#### Worker Functionality (To Be Removed) + +The codebase includes worker-specific functionality that will be removed as per the ticket requirements: + +1. **Instance Management Worker** + * Handles ODS instance creation, deletion, and renaming + * Transitions instance status (Pending → Completed, Pending_Delete → Deleted, etc.) + * Uses endpoints like `/instances/{instanceId}/completed` to report operation results + +2. **Health Check Worker** + * Monitors the health of ODS instances + * Records health check results in the `adminconsole.HealthChecks` table + * Uses health check endpoints to report monitoring results + +This worker functionality will be tagged in Git before removal to allow for future restoration if needed. + +## Endpoints Categorization + +### Core API Endpoints (to be preserved and migrated) + +1. **Tenant Management** + * `/tenants` (GET) - Retrieve all tenants + * `/tenants/{tenantId}` (GET) - Retrieve a specific tenant + +2. **ODS Instance Management** + * `/odsInstances` (GET) - Retrieve all ODS instances + * `/odsInstances/{id}` (GET) - Retrieve a specific ODS instance + +### Worker-Specific Endpoints (to be removed) + +1. **Instance Status Management** + * `/instances` (GET) - Get instances for worker use + * `/instances/{id}` (GET) - Get specific instance for worker use + * `/instances/{instanceId}/completed` (POST) - Used by worker processes + * `/instances/{instanceId}/deletefailed` (POST) - Used by worker processes + * `/instances/{instanceId}/renameFailed` (POST) - Used by worker processes + * `/instances/{instanceId}/renamed` (POST) - Used by worker processes + * `/instances/{instanceId}/deleted` (POST) - Used by worker processes + +2. **Health Check Management** + * `/healthcheck` (GET) - Used by health check worker + * `/healthcheck` (POST) - Used by health check worker + +## Tables to Be Migrated + +### Approach for Table Migration + +After analyzing the relationship between `[EdFi_Admin].[adminconsole].[Instances]` and `[EdFi_Admin].[dbo].[OdsInstances]`, we've determined that the additional information in the `adminconsole.Instances` table serves specific purposes not covered by the core `OdsInstances` table. + +Additionally, we've identified that there is already duplication between the following tables: + +* `adminconsole.OdsInstanceContexts` and `dbo.OdsInstanceContext` +* `adminconsole.OdsInstanceDerivatives` and `dbo.OdsInstanceDerivative` + +Since we're removing worker functionality, we should leverage the existing tables in the `dbo` schema rather than creating redundant tables in the `adminapi` schema. + +### Recommended Approach: Create a Single Extension Table + +Instead of migrating all three tables from the `adminconsole` schema, we will: + +1. Create a new extension table in the `adminapi` schema: + +```sql +[EdFi_Admin].[adminapi].[InstanceMetadata] +``` + +1. Continue using the existing tables in the `dbo` schema: + +```sql +[EdFi_Admin].[dbo].[OdsInstanceContext] +[EdFi_Admin].[dbo].[OdsInstanceDerivative] +``` + +The `InstanceMetadata` table will: + +* Have a foreign key to `dbo.OdsInstances` (OdsInstanceId) +* Store tenant association information (TenantId, TenantName) +* Store API access information (BaseUrl, ResourceUrl, OAuthUrl, Credentials) +* Remove worker-specific status tracking fields +* Reference the existing context and derivative information in `dbo` schema + +This approach provides a clean separation of concerns while eliminating redundancy in the database model. + +### Tables to Create + +1. `adminapi.InstanceMetadata` + * Extends the core `dbo.OdsInstances` table with additional API configuration + * Will not include worker-specific status fields + * Will maintain foreign key relationship to `dbo.OdsInstances` + +### Tables to Remove + +Tables exclusively used by worker processes should be removed: + +1. `adminconsole.Instances` - Will be replaced by `adminapi.InstanceMetadata` +2. `adminconsole.OdsInstanceContexts` - Redundant with `dbo.OdsInstanceContext` +3. `adminconsole.OdsInstanceDerivatives` - Redundant with `dbo.OdsInstanceDerivative` +4. `adminconsole.HealthChecks` - Specific to health check worker functionality + +## Proposed Endpoint Structure + +### Endpoints to Preserve + +| Current Endpoint | Proposed Endpoint | Purpose | +|------------------|-------------------|---------| +| `/adminconsole/tenants` | `/v2/tenants` | List all tenants | +| `/adminconsole/tenants/{tenantId}` | `/v2/tenants/{tenantId}` | Get tenant details | +| `/adminconsole/odsInstances/{id}` | `/v2/odsInstances/{id}/metadata` | Get instance details with data that previously was coming from `/adminconsole/odsInstances/{id}` endpoint| + +### Endpoints to Remove + +The following endpoints are exclusively used by worker processes and should be removed: + +1. `/adminconsole/instances` (GET) - Used by worker to get all instances +2. `/adminconsole/instances/{id}` (GET) - Used by worker to get instance details +3. `/adminconsole/instances` (POST) - Used to create new instance +4. `/adminconsole/instances` (PUT) - Used to update instance +5. `/adminconsole/instances` (DELETE) - Used to delete instance +6. `/adminconsole/instances/{instanceId}/completed` - Used to mark instance creation as completed +7. `/adminconsole/instances/{instanceId}/deletefailed` - Used to mark instance deletion as failed +8. `/adminconsole/instances/{instanceId}/renameFailed` - Used to mark instance rename as failed +9. `/adminconsole/instances/{instanceId}/renamed` - Used to mark instance rename as completed +10. `/adminconsole/instances/{instanceId}/deleted` - Used to mark instance deletion as completed +11. `/adminconsole/healthcheck` (GET) - Used to retrieve health check information +12. `/adminconsole/healthcheck` (POST) - Used to create health check entries + +## Implementation Plan + +### 1. Code Preservation Strategy + +To ensure we can easily restore the worker functionality in the future: + +1. Create separate, focused Pull Requests for each worker component: + * PR #1: Remove the Instance Management Worker functionality + * PR #2: Remove the Health Check Worker functionality + +2. This approach has several advantages: + * Each PR will represent a discrete, reversible change + * We can use `git revert` on specific PRs to restore functionality when needed + * Changes are more manageable and easier to review + * Documentation of the removed functionality is preserved in the PR history + +3. Document the PR numbers and purpose in the project documentation for future reference + +### 2. Database Migration + +1. Create the new extension table in the `adminapi` schema: + * `adminapi.InstanceMetadata` (replacing functionality from `adminconsole.Instances`) + * Ensure it has appropriate foreign key relationships to `dbo.OdsInstances` + +2. Remove redundant tables: + * Drop `adminconsole.Instances`, `adminconsole.OdsInstanceContexts`, and `adminconsole.OdsInstanceDerivatives` + * Remove the `adminconsole.HealthChecks` table and any other worker-specific tables + +### 3. Endpoint Updates + +1. Preserve the approved endpoints by updating their routes: + * `/tenants` → `/v2/tenants` + * `/tenants/{tenantId}` → `/v2/tenants/{tenantId}` + * `/odsInstances` → `/v2/odsInstances/{id}/metadata` + * `/odsInstances/{id}` → `/v2/odsInstances/{id}/metadata` + +2. Remove worker-specific endpoints: + * `/instances` (GET) + * `/instances/{id}` (GET) + * `/instances/{instanceId}/completed` + * `/instances/{instanceId}/deletefailed` + * `/instances/{instanceId}/renameFailed` + * `/instances/{instanceId}/renamed` + * `/instances/{instanceId}/deleted` + * `/healthcheck` + +### 4. Code Cleanup + +1. Remove worker-specific services and dependencies +2. Update entity models to reference the correct tables: + * Create new model for `adminapi.InstanceMetadata` + * Continue using existing models for `dbo.OdsInstanceContext` and `dbo.OdsInstanceDerivative` +3. Update AutoMapper profiles and other dependent code +4. Clean up any unused code related to worker functionality +5. Update dependency injection registrations + +### 5. Test Updates + +1. Update existing test projects: + * `EdFi.Ods.AdminConsole.DBTests` - Contains numerous tests for instance management commands + * Update command tests (CompleteInstance, AddInstance, DeleteInstance, etc.) + * Update query tests (GetInstanceById, etc.) + +2. Create new tests for the migrated endpoints: + * Test `/v2/odsInstances/{id}/metadata` endpoints + * Test `/v2/tenants` endpoints + * Verify database interactions with the new `adminapi.InstanceMetadata` table + +3. Update integration tests: + * Modify tests that rely on `adminconsole` schema tables + * Update tests to use the new endpoint paths + * Ensure proper integration with the existing `/v2/odsInstances` endpoints diff --git a/docs/design/Education-organization-Endpoints.md b/docs/design/Education-organization-Endpoints.md new file mode 100644 index 000000000..6a9140c4a --- /dev/null +++ b/docs/design/Education-organization-Endpoints.md @@ -0,0 +1,161 @@ +# Education Organizations Endpoints + +## Overview + +Provides a consolidated view of education organizations across all Ed-Fi ODS +instances through REST API endpoints. The data is refreshed on a scheduled +basis. + +## Features + +* **REST API Endpoints:** + * `GET /{version}/educationOrganizations` - Returns all education + organizations from all instances + * `GET /{version}/educationOrganizations/{instanceId}` - Returns education + organizations for a specific instance + * `POST /{version}/educationOrganizations/refresh` - Refreshes the education + organizations for all instances + * `POST /{version}/educationOrganizations/refresh/{instanceId}` - Refreshes + the education organizations for specific instance + +* **Data Refresh:** + * Quartz.NET Scheduled Job: Runs every 6 hours by default, + automatically refreshing the data in the background + * Manual Refresh: API endpoints are available for manually triggering an + education organizations data refresh + +* **Cross-Database Support:** + * Works with both SQL Server and PostgreSQL + * Uses C# service layer for dynamic database connections and efficient data + refresh + +## Database Schema + +### Tables + +* `adminapi.EducationOrganizations` - Stores consolidated education organization + data + + ```sql + CREATE TABLE [adminapi].[EducationOrganizations] ( + [Id] INT IDENTITY(1,1) NOT NULL, + [InstanceId] INT NOT NULL, + [InstanceName] NVARCHAR(100) NOT NULL, + [EducationOrganizationId] INT NOT NULL, + [NameOfInstitution] NVARCHAR(75) NOT NULL, + [ShortNameOfInstitution] NVARCHAR(75) NULL, + [Discriminator] NVARCHAR(128) NOT NULL, + [ParentId] INT NULL, + [OdsDatabaseName] NVARCHAR(255) NULL, + [LastRefreshed] DATETIME2 NOT NULL DEFAULT GETUTCDATE(), + [LastModifiedDate] DATETIME2 NULL, + CONSTRAINT [PK_EducationOrganizations] PRIMARY KEY ([Id]) + ); + ``` + +## API Usage Examples + +### Get All Education Organizations + +```http +GET /v2/educationOrganizations +Authorization: Bearer +``` + +### Get Education Organizations for Specific Instance + +```http +GET /v2/educationOrganizations/123 +Authorization: Bearer +``` + +## Configuration + +### Quartz.NET Job Scheduling + +Add to `appsettings.json`: + +```json +{ + "AppSettings": { + "EducationOrgsRefreshIntervalInHours": 6 + } +} +``` + +### Database Connection + +The system uses the existing AdminAPI database connection and dynamically +connects to ODS instance databases based on the `OdsInstances` table +configuration. + +## Architecture + +### Service Layer + +The service files can be maintained in a common project and shared between the +V1 and V2 projects to avoid code duplication. + +* `IGetEducationOrganizationQuery` - Main query handling interface + +* `GetEducationOrganizationQuery` - Implementation of database context query logic + for reading the EducationOrganizations + +The `RefreshEducationOrganizationCommand` service layer implements a +comprehensive C# solution to aggregate education organization data across +multiple ODS instances and persist it into the +[adminapi].[EducationOrganizations] table. + +**Key Components:** + +* **Dynamic Database Connectivity**: Reads ODS instance connection strings from + the `OdsInstances` table, decrypts them, and then establishes connections to + each ODS database dynamically +* **Optimize Database Connectivity**: Keep connection pooling enabled. Group + connection strings by server, and consider single-connection, multi-database + queries. Execute queries in parallel with controlled concurrency (e.g., 8–15 + simultaneous executions) +* **Multi-Database Support**: Handles both SQL Server and PostgreSQL with + database-specific query implementations +* **Parent Hierarchy Logic**: Implements the same parent relationship logic from + the AWS Lambda reference using COALESCE joins to establish education + organization hierarchies + [LambdaFunction](https://github.com/edanalytics/startingblocks_oss/blob/efc423212930e01f0166033d97be392d3a675999/lambdas/TenantResourceTreeLambdaFunction/index.mjs#L100) +* **Robust Error Handling**: Continues processing other instances even if one + fails, with detailed logging for troubleshooting + +**Core Methods:** + +* `RefreshDataAsync(int? instanceId)` - Main orchestration method that clears + existing data and processes ODS instances +* `GetOdsInstancesToProcessAsync(int? instanceIdFilter)` - Queries the + OdsInstances table to get connection information +* `ProcessOdsInstanceAsync(OdsInstanceInfo odsInstance)` - Processes a single + ODS instance with validation and error handling +* `QueryEducationOrganizationsFromOdsAsync(OdsInstanceInfo odsInstance)` - + Routes to database-specific query methods +* `QueryEducationOrganizationsSqlServerAsync()` / + `QueryEducationOrganizationsPostgreSqlAsync()` - Database-specific + implementations that execute the parent hierarchy queries + +**Data Flow:** + +1. Service reads ODS instance configurations from the AdminAPI database +2. For each instance, establishes a direct connection using the stored + connection string +3. Executes complex SQL queries with COALESCE logic to determine parent + relationships +4. Maps raw data to Entity Framework entities with proper validation +5. Performs batch inserts using Entity Framework for optimal performance +6. Continues processing remaining instances even if individual instances fail + +### Background Jobs + +* `EducationOrganizationRefreshJob` - Quartz.NET job for scheduled refresh + +* Runs with `DisallowConcurrentExecution` to prevent overlapping executions + +### Controllers + +* `EducationOrganizationsController` - REST API endpoints for read-only access +* Includes proper authorization, error handling, and logging diff --git a/docs/design/INTEGRATE-ADMINAPI.V1.md b/docs/design/INTEGRATE-ADMINAPI.V1.md new file mode 100644 index 000000000..1c2f6430d --- /dev/null +++ b/docs/design/INTEGRATE-ADMINAPI.V1.md @@ -0,0 +1,489 @@ + +# Integration Design: EdFi.Ods.AdminApi.V1 with EdFi.Ods.AdminApi (V2) + +## Overview + +This document outlines the design for integrating EdFi.Ods.AdminApi.V1 endpoints +into the existing EdFi.Ods.AdminApi (V2) solution. The integration will maintain +backward compatibility while leveraging the enhanced architecture of V2. + +### Goals + +* Maintain backward compatibility for existing V1 API clients +* Leverage V2's enhanced architecture and infrastructure +* Minimize code duplication between versions +* Provide clear migration path for V1 to V2 +* Centralize common functionality + +### Integration Strategy + +* **Phase 1**: Clean up and modernize V1 codebase +* **Phase 2**: Merge projects and consolidate infrastructure +* **Phase 3**: Implement unified endpoint mapping +* **Phase 4**: Testing and Validation +* **Phase 5**: V1/V2 Multi-Tenancy Integration Strategy +* **Phase 6**: Docker setup + +--- + +## Phase 1: EdFi.Ods.AdminApi.V1 Cleanup and Modernization + +### 1.1 Remove Legacy Security Components + +**Objective**: Simplify V1 codebase by removing Ed-Fi ODS 5.3 compatibility and standardizing on V6. + +**Tasks**: + +* Remove `EdFi.SecurityCompatiblity53.DataAccess` dependency +* Remove `OdsSecurityVersionResolver` and related version detection logic +* Remove conditional service implementations (V53Service vs V6Service) +* Update all code flows to use only V6 services +* Rename project assemblies from `EdFi.Ods.AdminApi` to `EdFi.Ods.AdminApi.V1` + +**Example Transformation**: + +```csharp +// BEFORE: Version-dependent service resolution +private readonly IOdsSecurityModelVersionResolver _resolver; +private readonly EditResourceOnClaimSetCommandV53Service _v53Service; +private readonly EditResourceOnClaimSetCommandV6Service _v6Service; + +public EditResourceOnClaimSetCommand(IOdsSecurityModelVersionResolver resolver, + EditResourceOnClaimSetCommandV53Service v53Service, + EditResourceOnClaimSetCommandV6Service v6Service) +{ + _resolver = resolver; + _v53Service = v53Service; + _v6Service = v6Service; +} + +public void Execute(IEditResourceOnClaimSetModel model) +{ + var securityModel = _resolver.DetermineSecurityModel(); + switch (securityModel) + { + case EdFiOdsSecurityModelCompatibility.ThreeThroughFive or EdFiOdsSecurityModelCompatibility.FiveThreeCqe: + _v53Service.Execute(model); + break; + case EdFiOdsSecurityModelCompatibility.Six: + _v6Service.Execute(model); + break; + default: + throw new EdFiOdsSecurityModelCompatibilityException(securityModel); + } +} + +// AFTER: Simplified V6-only implementation +public class EditResourceOnClaimSetCommand(EditResourceOnClaimSetCommandV6Service v6Service) +{ + private readonly EditResourceOnClaimSetCommandV6Service _v6Service = v6Service; + + public void Execute(IEditResourceOnClaimSetModel model) + { + _v6Service.Execute(model); + } +} +``` + +### 1.2 Project Structure Standardization + +**Objective**: Align V1 project structure with V2 conventions and dependency management. + +**Tasks**: + +* Convert V1 project to use `Directory.Packages.props` for version management +* Remove explicit version numbers from V1 project file package references +* Ensure V1 project builds successfully with V6-only dependencies +* Validate all unit tests pass after cleanup + +--- + +## Phase 2: Project Merge and Infrastructure Consolidation + +### 2.1 Eliminate Duplicate Infrastructure Classes + +**Objective**: Consolidate common infrastructure components to reduce maintenance overhead. + +**Classes to Consolidate** (merge from V1 to V2, then remove from V1): + +* `AdminApiDbContext.cs` - Database context configuration +* `AdminApiEndpointBuilder.cs` - Endpoint registration patterns +* `AdminApiVersions.cs` - API versioning constants +* `CloudOdsAdminApp.cs` - Cloud deployment configurations +* `CommonQueryParams.cs` - Shared query parameter models +* `DatabaseEngineEnum.cs` - Database engine enumeration +* `EndpointRouteBuilderExtensions.cs` - Route building extensions +* `Enumerations.cs` - Common enumerations +* `IMarkerForEdFiOdsAdminAppManagement.cs` - Assembly markers +* `InstanceContext.cs` - Instance context management +* `OdsSecurityVersionResolver.cs` - Security version resolution (remove from V1) +* `OperationalContext.cs` - Operational context management +* `ValidatorExtensions.cs` - Validation helper extensions +* `WebApplicationBuilderExtensions.cs` - Application builder extensions +* `WebApplicationExtensions.cs` - Application configuration extensions +* **Security folder and all classes** - Use V2 security implementation +* **Connect\Register and Connect\Token endpoints** - Use V2 implementation +* **Artifacts folder** - Remove from V1, use V2 artifacts +* **Information feature** - Remove from V1, use V2 implementation + +### 2.2 DataAccess Layer Strategy + +**Objective**: Maintain V1 compatibility by preserving Ed-Fi ODS 6.x DataAccess +implementations while supporting runtime mode switching with unified connection +strings. + +**Tasks**: + +* Copy Ed-Fi ODS 6.x Admin.DataAccess and Security.DataAccess code implementation files directly to AdminAPI V1 project +* Avoid handling divergence between 6.x and 7.x Security and Admin DataAccess usages while maintaining V1 API compatibility + +* **Isolation Benefits**: + * V1 maintains stable DataAccess layer independent of V2 upgrades + * Eliminates version compatibility complexity in shared DataAccess components + * Reduces risk of breaking V1 functionality when V2 adopts newer Ed-Fi ODS versions + +### 2.3 Database Context Setup Strategy + +* **Runtime Mode Configuration**: Add `adminApiMode` setting to control which version endpoints are active: + + ```json + { + "AppSettings": { + "adminApiMode": "v1", // or "v2" + "MultiTenancy": false, + "EnableAdminConsoleAPI": true + }, + "ConnectionStrings": { + // Single connection strings for both modes + "EdFi_Admin": "Server=.;Database=EdFi_Admin;Integrated Security=true", + "EdFi_Security": "Server=.;Database=EdFi_Security;Integrated Security=true" + } + } + ``` + +* **Mode-Aware DbContext Registration**: Configure DbContext based on `adminApiMode`: + + ```csharp + public static void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + var adminApiMode = configuration.GetValue("AppSettings:adminApiMode")?.ToLower(); + var adminConnectionString = configuration.GetConnectionString("EdFi_Admin"); + var securityConnectionString = configuration.GetConnectionString("EdFi_Security"); + + switch (adminApiMode) + { + case "v1": + // Register V1 DbContext with 6.x DataAccess layer + services.AddDbContext(options => + options.UseSqlServer(adminConnectionString)); + services.AddDbContext(options => + options.UseSqlServer(securityConnectionString)); + break; + + case "v2": + // Register V2 DbContext with 7.x DataAccess layer + services.AddDbContext(options => + options.UseSqlServer(adminConnectionString)); + services.AddDbContext(options => + options.UseSqlServer(securityConnectionString)); + break; + + default: + throw new InvalidOperationException($"Invalid adminApiMode: {adminApiMode}. Must be 'v1' or 'v2'"); + } + } + ``` + +### 2.4 Project Type Conversion + +**Objective**: Convert V1 from standalone application to class library. + +**Tasks**: + +* Convert `EdFi.Ods.AdminApi.V1` project to class library type +* Remove `appsettings.json` files from V1 project +* Move V1-specific configuration to V2 project `appsettings.json` +* Move V1 E2E tests to V2 E2E tests folder structure + +--- + +## Phase 3: Endpoint Mapping and API Versioning + +### 3.1 Implement V1 Endpoint Mapping + +**Objective**: Create unified endpoint registration. + +**Implementation** (add to `WebApplicationExtensions.cs`): + +```csharp + public static void MapAdminApiEndpoints(this WebApplication app) + { + var adminApiMode = app.Configuration.GetValue("AppSettings:adminApiMode")?.ToLower(); + + // Always register unversioned endpoints + app.MapConnectEndpoints(); + app.MapDiscoveryEndpoint(); + app.MapHealthCheckEndpoints(); + + switch (adminApiMode) + { + case "v1": + app.MapAdminApiV1FeatureEndpoints(); + break; + + case "v2": + app.MapAdminApiV2FeatureEndpoints(); + app.MapAdminConsoleFeatureEndpoints(); + break; + + default: + throw new InvalidOperationException($"Invalid adminApiMode: {adminApiMode}"); + } + } +``` + +### 3.2 Mode-Aware API Configuration + +**Objective**: Configure information endpoint responses and implement endpoint filtering based on the `adminApiMode` setting, including validation middleware for version-specific endpoint access. + +**Implementation**: + +**Version-Aware Discovery Endpoint**: Update discovery/information response based on the mode: + +```csharp +public class ReadInformation : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + // Map endpoint implementation + } + internal static InformationResult GetInformation() + { + return adminApiMode switch + { + "v1" => new InformationResult + { + Version = "1.1", + Build = buildVersion + }, + "v2" => new DiscoveryResponse + { + Version = "2.3", + Build = buildVersion + }, + _ => throw new InvalidOperationException($"Invalid adminApiMode: {adminApiMode}") + }; + } +} +``` + +* **Mode Validation Middleware**: Return 400 errors for wrong endpoint usage: + + ```csharp + public class AdminApiModeValidationMiddleware + { + // Constructor: Get adminApiMode from configuration (default: "v2") + + public async Task InvokeAsync(HttpContext context) + { + // Skip validation for unversioned endpoints (/connect/, /health, /.well-known/) + if (IsUnversionedEndpoint(path)) + continue to next middleware; + + // Extract version from path (/v1/ or /v2/) + var requestedVersion = GetVersionFromPath(path); + + // If requested version doesn't match configured mode + if (requestedVersion != _adminApiMode) + { + // Return 400 with descriptive error message + return BadRequest("Wrong API version for this instance mode"); + } + + // Continue to next middleware + await _next(context); + } + + // Helper: Check if endpoint is unversioned (auth, health, discovery) + private static bool IsUnversionedEndpoint(string path) { ... } + + // Helper: Extract "v1" or "v2" from URL path + private static string GetVersionFromPath(string path) { ... } + } + + ``` + +### 3.3 API Versioning Strategy + +**URL Structure**: + +* V1 endpoints: `/v1/applications`, `/v1/claimsets`, etc. +* V2 endpoints: `/v2/applications`, `/v2/claimsets`, etc. +* Default (unversioned): Connect endpoints (Register and Token), discovery endpoint, and health check endpoints remain unversioned. + +--- + +## Phase 4: Testing and Validation + +**Objective**: Enhance test coverage for V1 project and ensure seamless integration with V2 test infrastructure. + +**Tasks**: + +* Add unit tests for uncovered areas using NUnit and Shouldly patterns consistent with V2 + +### 4.1 Integration Test Consolidation + +**Objective**: Merge V1 integration tests with V2 test infrastructure while maintaining test isolation and version-specific database compatibility. + +**Tasks**: + +* **Consolidate Test Projects**: Merge V1 `*.DBTests` projects into V2 database testing infrastructure: + * Move V1 integration tests to `EdFi.Ods.AdminApi.DBTests` project + * Organize tests in version-specific namespaces: `EdFi.Ods.AdminApi.DBTests.V1` and `EdFi.Ods.AdminApi.DBTests.V2` + * Maintain separate test base classes for V1 and V2 to handle different DbContexts + * Ensure V1 and V2 tests use completely separate test databases + +### 4.2 End-to-End Test Migration + +**Objective**: Consolidate V1 E2E tests into V2 test structure while maintaining version-specific validation. + +**Tasks**: + +* **E2E Test Organization**: Move V1 E2E tests to V2 `E2E Tests` folder with version-specific subdirectories: + + ```md + + E2E Tests/ + ├── V1/ + │ ├── Applications/ + │ ├── ClaimSets/ + │ + ├── V2/ + + ``` + + * Update V1 Postman collections to use `/v1/` URL prefix + * Create combined collection supporting both V1 and V2 endpoints + * Add version-specific environment variables + * Test version routing (v1 vs v2 vs unversioned URLs) + * Add tests that validate V1 and V2 can operate simultaneously without conflicts + +## Phase 5: V1/V2 Multi-Tenancy Integration Strategy + +**Objective**: Strategy for maintaining multi-tenancy support in V2 while ensuring V1 endpoints continue to work without multi-tenancy requirements during the integration of AdminAPI V1 and V2. + +**Tasks**: + +* **Define Version-Aware Multi-Tenancy Middleware**: Enhance + `TenantResolverMiddleware` to detect `adminApiMode` and apply + multi-tenancy rules accordingly. + +```csharp + +private readonly string _adminApiMode; + +public TenantResolverMiddleware(RequestDelegate next, IConfiguration configuration) +{ + _adminApiMode = configuration.GetValue("AppSettings:adminApiMode")?.ToLower() ?? "v2"; +} +private static bool IsV1Mode(HttpContext context) +{ + return string.Equals(_adminApiMode, "v1", StringComparison.InvariantCultureIgnoreCase); +} + +public async Task InvokeAsync(HttpContext context, RequestDelegate next) +{ + + // Check if this is a V1 endpoint + if (IsV1Mode(context)) + { + // For V1 endpoints, skip multi-tenancy validation entirely + await next.Invoke(context); + return; + } + + if (multiTenancyEnabled) + { + } +} + +``` + +## Phase 6: Docker Setup + +**Objective**: Update Docker files to support mode-based Admin API deployment with schema-specific database containers selected at build time. + +**Tasks**: + +* **Multiple Dockerfile Strategy**: Create separate v1 and v2 folders to organize version-specific Docker files: + +```md + +Docker/ +├── dev.mssql.Dockerfile +├── dev.pgsql.Dockerfile +├── v1/ +│ ├── db.mssql.admin.Dockerfile # 6.x database schema +│ ├── db.pgsql.admin.Dockerfile # 6.x database schema +│ └── Compose/ +│ └── pgsql/ +│ └── compose-build-dev.yml +└── v2/ + ├── db.mssql.admin.Dockerfile # 7.x database schema + ├── db.pgsql.admin.Dockerfile # 7.x database schema + └── Compose/ + └── pgsql/ + └── compose-build-dev.yml + +``` + +* Update Build Stage for V1 Project Integration +**Files to modify**: dev.mssql.Dockerfile and dev.pgsql.Dockerfile + +```docker + +COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.V1/ +COPY --from=assets ./Application/EdFi.Ods.AdminApi.V1 EdFi.Ods.AdminApi.V1/ +``` + +* **Mode-Based Docker Compose Configuration**: Select appropriate database Dockerfile based on mode: + + **V1 Mode Compose** (`Docker/v1/Compose/pgsql/compose-build-dev.yml`): + + ```yaml + services: + db-admin: + build: + context: ../../../../ + dockerfile: Docker/v1/db.pgsql.admin.Dockerfile # 6.x schema + environment: + + + adminapi: + build: + context: ../../../../ + dockerfile: Docker/dev.pgsql.Dockerfile + environment: + AppSettings__adminApiMode: "v1" + depends_on: + - db-admin + container_name: ed-fi-adminapi-v1 + ``` + + **V2 Mode Compose** (`Docker/v2/Compose/pgsql/compose-build-dev.yml`): + + ```yaml + services: + db-admin: + build: + context: ../../../../ + dockerfile: Docker/v2/db.pgsql.admin.Dockerfile # 7.x schema + environment: + + adminapi: + environment: + AppSettings__adminApiMode: "v2" + depends_on: + - db-admin + container_name: ed-fi-adminapi-v2 + ``` diff --git a/docs/design/INTEGRATE-HEALTHCHECK-SERVICE.md b/docs/design/INTEGRATE-HEALTHCHECK-SERVICE.md new file mode 100644 index 000000000..48e30b9ae --- /dev/null +++ b/docs/design/INTEGRATE-HEALTHCHECK-SERVICE.md @@ -0,0 +1,62 @@ +# Integrating EdFi.Ods.AdminApi.HealthCheck into EdFi.Ods.AdminApi with Quartz.NET + +## Overview + +This document describes the design and process for integrating the +`EdFi.Ods.AdminApi.HealthCheck` into the `EdFi.Ods.AdminApi` application, +leveraging Quartz.NET for scheduled and on-demand execution of health checks. + +--- + +## Goals + +* Enable scheduled health checks of ODS API instances via Quartz.NET. +* Allow on-demand triggering of health checks via an API endpoint. +* Ensure only one health check job runs at a time to prevent data conflicts. +* Centralize health check logic in `EdFi.Ods.AdminApi.HealthCheck`. + +--- + +## Architecture + +### Components + +* **HealthCheckService**: Service class that performs health checks across tenants and instances. +* **HealthCheckJob**: Quartz.NET job that invokes `HealthCheckService.Run()`. +* **Quartz.NET Scheduler**: Manages scheduled and ad-hoc job execution. +* **HealthCheckTrigger Endpoint**: API endpoint to trigger health checks on demand. + +--- + +## Process Flow + +### 1. Service Registration + +* Register `HealthCheckService` and its dependencies in the DI container (typically as `scoped` or `transient`). +* Register `HealthCheckJob` with Quartz.NET using `AddQuartz` and `AddQuartzHostedService`. + +### 2. Scheduling with Quartz.NET + +* Configure Quartz.NET to schedule `HealthCheckJob` at a configurable interval (e.g., every 10 minutes, using `HealthCheckFrequencyInMinutes` from configuration). +* Use the `[DisallowConcurrentExecution]` attribute on `HealthCheckJob` to prevent overlapping executions. + +### 3. On-Demand Triggering + +* Implement an API endpoint (e.g., `/v2/healthcheck`) in `EdFi.Ods.AdminApi`. Note: Grouped with `v2` endpoints for consistency. +* The endpoint uses `ISchedulerFactory` to schedule an immediate, one-time execution of `HealthCheckJob`. + +### 4. Concurrency Control + +* `[DisallowConcurrentExecution]` ensures only one instance of `HealthCheckJob` runs at a time, regardless of trigger source (scheduled or on-demand). + +--- + +## Configuration + +* **appsettings.json**: + * `HealthCheck:HealthCheckFrequencyInMinutes`: Controls the schedule interval. + Set to 0 to disable scheduled health checks. + * `AppSettings:EnableAdminConsoleAPI`: Enables or disables the health check + API endpoint and scheduled health checks. + +Please refer to the [POC PR #323](https://github.com/Ed-Fi-Alliance-OSS/AdminAPI-2.x/pull/323) for implementation details and code examples. diff --git a/docs/design/Integrate-Instance-Management.md b/docs/design/Integrate-Instance-Management.md new file mode 100644 index 000000000..93b12daaa --- /dev/null +++ b/docs/design/Integrate-Instance-Management.md @@ -0,0 +1,141 @@ +# Integrating EdFi.AdminConsole.InstanceManagement into EdFi.Ods.AdminApi with Quartz.NET + +This document describes the design and process for integrating the `Ed-Fi-Admin-Console-Instance-Management-Worker-Process` +into the `EdFi.Ods.AdminApi` application, leveraging Quartz.NET for on-demand execution of Instance-Management-Worker. + +## Overview of the InstanceManagement solution + +These are the 4 projects that create the Instance Management Worker solution. `EdFi.AdminConsole.InstanceMgrWorker.Configuration` is +the only project that would be copied over to Admin API. + +1. EdFi.AdminConsole.InstanceManagementWorker (Console Application, where process starts) +2. EdFi.AdminConsole.InstanceMgrWorker.Configuration (Manage ods database creation and deletion) +3. EdFi.AdminConsole.InstanceMgrWorker.Core (main core functionality, mainly call Admin API features) +4. EdFi.Ods.AdminConsole.InstanceMgrWorker.Core.UnitTests (17 Unit tests) + +### Restoring and deleting the instance database + +The `EdFi.AdminConsole.InstanceMgrWorker.Configuration` project is the responsible to do these tasks. +It should not change given that we still need these tasks. + +#### Restoring and deleting a mssql + +The process to create the mssql database is the following: + +1. Reads the logical file names from the backup using `RESTORE FILELISTONLY`. +2. Executes a `RESTORE DATABASE` command to create the new database from the backup, moving the data and log files to the correct locations. + +To delete an ods database instance it simply executes `DROP DATABASE` + +#### Restoring and deleting a pgsql + +To create the ods instance database on pgsql it simply executes the `CREATE DATABASE "new-database" TEMPLATE "template-database"` command. + +To delete it, the command is `DROP DATABASE IF EXISTS "database"`. + +### HTTP call to Admin API + +The `EdFi.AdminConsole.InstanceMgrWorker.Core` is the responsible to do these tasks. +This project can be removed, given that `Instance-Management-Worker` lives now with Admin API. +To get tenants and instances, and other other transactions we do in this project, we can use the +`Database/Commands` classes + +### Project execution + +The `EdFi.AdminConsole.InstanceManagementWorker` is the responsible to do these tasks. +This project can be removed as well. Its main tasks is to loop through tenants and instances +to process instances to be created and instances to be deleted. +The component that performs these tasks will be integrated as new **Features** (Features layer) in `EdFi.Ods.AdminApi` + +## New Architecture + +### Components + +#### 1. InstanceManagementCompleteService Feature + +Service that performs instance management creation for given instance +`InstanceManagementCompleteService` is triggered on every call to `POST /adminconsole/instances` + +##### InstanceManagementCompleteService will + +* Create new records on ApiClients, OdsInstances, potentially OdsInstanceContexts and OdsInstanceDerivatives as well, etc. +* Create the database itself, if it doesn't exist. +* Change instance status from `Pending` to `Completed` on `adminconsole.Instances`. +In this case `Completed` means that the instance is created, and it's fully functional. + +#### 2. InstanceManagementDeleteService Feature + +Service that performs instance management deletion for given instance +`InstanceManagementDeleteService` is triggered on every call to `DELETE /adminconsole/instances` + +##### InstanceManagementDeleteService will + +* Delete records on ApiClients, OdsInstances, OdsInstanceContexts, OdsInstanceDerivatives as well, etc. +* Delete the database itself. +* Change instance status from `Pending_Delete` to `Deleted` on `adminconsole.Instances`. + +#### 3. InstanceManagementRenameService Feature + +Service that performs instance management renaming for given instance +`InstanceManagementRenameService` is triggered on every call to `PUT /adminconsole/instances` + +##### InstanceManagementRenameService will + +* Update the instance information on OdsInstances, OdsInstanceContexts, OdsInstanceDerivatives, etc. +* Rename the database itself if the instance name actually changed. +* Change instance status from `Pending_Rename` to `Completed` on `adminconsole.Instances`. + +#### 4. InstanceManagementCompleteJob + +Quartz.NET job that invokes `InstanceManagementCompleteService.RunAsync()` + +#### 5. InstanceManagementDeleteJob + +Quartz.NET job that invokes `InstanceManagementDeleteService.RunAsync()` + +#### 6. InstanceManagementRenameJob + +Quartz.NET job that invokes `InstanceManagementRenameService.RunAsync()` + +### Configuration + +There are a number of application settings that need to be added to Admin API + +| Name | Description | +| --- | --- | +| AppSettings:OverrideExistingDatabase | When a creation of a new instance is requested, but the database already exists. | +| AppSettings:SqlServerBakFile | Backup Sql Server file to use as a template when creating a new ods instance database. | +| AppSettings:MaxRetryAttempts | When calling Ods API and Admin API, how many times to retry when connection is not successful | +| DatabaseProvider | Database engine | + +There are a number of application settings that we **DO NOT** need anymore in Admin API + +| Name | Description | +| --- | --- | +| AdminApiSettings:AdminConsoleTenantsURL | Call to get Tenants from Admin API | +| AdminApiSettings:AdminConsoleInstancesURL | Call to get Instances from Admin API | +| AdminApiSettings:AdminConsoleCompleteInstancesURL | Call to complete Instances from Admin API | +| AdminApiSettings:AdminConsoleInstanceDeletedURL | Call to delete Instances from Admin API | +| AdminApiSettings:AdminConsoleInstanceDeleteFailedURL | Call when a deletion of a instance database has failed | +| AdminApiSettings:AccessTokenUrl | Call to get access token | +| AdminApiSettings:ClientId | Client Id to get authenticated on Admin API | +| AdminApiSettings:ClientSecret | Client Secret Id to get authenticated on Admin API | +| AdminApiSettings:GrantType | Grant Type | +| AdminApiSettings:Scope | Scope | + +### Connection Strings + +Two new connection strings need to be added to Admin API + +| Name | Description | +| --- | --- | +| EdFi_Master | To get authenticated on Database Engine (mssql or pgsql) | +| EdFi_Ods | Where the new Instance database is created or deleted | + +### Cleanup + +Given the new architecture, it should be safe to remove these 3 endpoints + +1. /adminconsole/instances/{instanceId}/deletefailed +2. /adminconsole/instances/{instanceId}/renameFailed +3. /adminconsole/instances/{instanceId}/completed diff --git a/docs/design/adminconsole/APIS-FOR-ADMIN-CONSOLE.md b/docs/design/adminconsole/APIS-FOR-ADMIN-CONSOLE.md new file mode 100644 index 000000000..589c6ef87 --- /dev/null +++ b/docs/design/adminconsole/APIS-FOR-ADMIN-CONSOLE.md @@ -0,0 +1,133 @@ +# REST API Support for Admin Console + +This document describes the new interfaces and data storage requirements to be +fulfilled directly in the Ed-Fi ODS/API Admin API 2 application, in support of +the Ed-Fi Admin Console and the two worker processes (Instance Management, +Health Check). + +## System Context + +```mermaid +C4Container + title "Admin API Containers" + + System(AdminConsole, "Ed-Fi Admin Console", "A web application for managing ODS/API Deployments") + + System_Boundary(backend, "Backend Systems") { + + Boundary(b0, "Admin API") { + Container(AdminAPI, "Ed-Fi Admin API 2", "BFF for Admin Console") + + Container(HealthCheck, "Admin API Health
Check Worker", "Asynch ODS/API communication") + UpdateElementStyle(HealthCheck, $bgColor="silver") + Container(Instance, "Admin API Instance
Management Worker", "Asynch DB management") + UpdateElementStyle(Instance, $bgColor="silver") + } + + Boundary(b2, "Databases") { + ContainerDb(Security, "EdFi_Security.dbo.*", "Configuration for ODS/API") + + Container_Boundary(b3, "EdFi_Admin DB") { + ContainerDb(dbo, "dbo.*", "Configuration for ODS/API") + ContainerDb(adminapi, "adminapi.*", "Configuration for Admin API") + ContainerDb(adminconsole, "adminconsole.*", "End-user requests and config") + } + } + } + + Rel(AdminConsole, AdminAPI, "Issues HTTP requests") + + Rel(HealthCheck, AdminAPI, "Reads ODS/API connections,
Writes health info") + UpdateRelStyle(HealthCheck, AdminAPI, $offsetX="0", $offsetY="0") + + Rel(Instance, AdminAPI, "Reads instance requests,
Write instance status") + UpdateRelStyle(Instance, AdminAPI, $offsetY="90", $offsetX="-140") + + Rel(AdminAPI, Security, "Reads and writes") + UpdateRelStyle(AdminAPI, Security, $offsetX="-40", $offsetY="15") + + Rel(AdminAPI, dbo, "Reads and writes") + UpdateRelStyle(AdminAPI, dbo, $offsetX="-25", $offsetY="-10") + + Rel(AdminAPI, adminapi, "Reads and writes") + UpdateRelStyle(AdminAPI, adminapi, $offsetX="-25", $offsetY="0") + + Rel(AdminAPI, adminconsole, "Reads and writes") + UpdateRelStyle(AdminAPI, adminconsole, $offsetX="0", $offsetY="50") + + UpdateLayoutConfig($c4ShapeInRow="1", $c4BoundaryInRow="2") +``` + +## Solution Design + +We are going to expose the Admin Console endpoints required for the application +in Admin API 2. It will be hosted as a different definition in Swagger and the +base path will be `/adminconsole/{resource}`. + +The original Admin Console source code received by the Ed-Fi Alliance included +support for several functional areas that will be excluded in the version 1.0 +release of the Ed-Fi Admin Console. The following table lists the functional +areas and their status. Functions that are included in the release plan will be +described in more detail below. + +| Function | Status | Notes | +| ------------------- | ------ | -------------------------------------------------------------- | +| Tenant management | ✅ | Will still require manual updates to ODS/API's tenant settings | +| Instance management | ✅ | Asynchronously supported by the Instance Management Worker | +| Health check | ✅ | Asynchronously supported by the Health Check Worker | +| Vendors | ✅ | Directly supporting `dbo.Vendors` | +| Applications | ✅ | Directly managing `dbo.Applications` (and `dbo.ApiClients`?) | +| Claimsets | ✅ | (Read-only?) list of available claimsets | +| Onboarding Wizard | ❌ | May restore in the future | +| User Profile | ❌ | May restore in the future | +| Permissions | ❌ | May restore in the future | + +### Tenant Management + +Path segment: `/adminconsole/tenants`. + +Initially, the tenant API will follow the ODS/API's pattern: it will be stored +in the appsettings file. Hence only the `GET` request is supported; modification +support will be restored in the future when tenants move to a database table. + +See [Tenant Management Data](./TENANT-DATA.md) for information about the +structure and data of a `tenant` object. + +### ODS Instances + +**Path segment: `/adminconsole/odsInstances`.** + +Supports full create, read, and update operations from the administrative user's +point of view. Does not support delete. Some data that will be managed with +`instance` (described below) should not be available to an end-user and will not +be included in this endpoint. Notably, the `clientId` and `clientSecret` should +be empty strings when responding to the `odsinstances` endpoint requests. + +See [Instance Management Data](./INSTANCE-DATA.md) for more information about +this endpoint and its data. + +### Instances + +**Path segment: `/adminconsole/instances`.** + +Supports all CRUD operations, for use by the [Instance Management +Worker](./INSTANCE-MANAGEMENT.md), [Health Check +Worker](./HEALTH-CHECK-WORKER.md), or a system administrator directly accessing +the API with appropriate credentials. Must be secured by Role name in the JSON +Web Token (JWT). Define the role name in appSettings. For more about role +management, also see [Keycloak Configuration](./KEYCLOAK.md). + +**Path segment: `/adminconsole/instances/{id}/completed`** + +POST operation for the instance management worker only. Supports updating only +the status of an instance, without having to provide a body. For the given ID, +sets the following values on the instance: + +* `status`: "Completed" +* `completedAt` set to "now" + * Started is being set the same initially; in the future there may be a more + sophisticated process that updates the record as "in progress" with a start + date. + +See [Instance Management Data](./INSTANCE-DATA.md) for more information about +the structure and data of the two `instance` objects. diff --git a/docs/design/adminconsole/HEALTH-CHECK-WORKER.md b/docs/design/adminconsole/HEALTH-CHECK-WORKER.md new file mode 100644 index 000000000..353900283 --- /dev/null +++ b/docs/design/adminconsole/HEALTH-CHECK-WORKER.md @@ -0,0 +1,183 @@ +# Health Check Worker + +This document describes the work performed by the Admin API 2 application and +its associated Health Check Worker for retrieving and storing record counts from +the ODS/API. + +## Containers + +```mermaid +C4Container + title "Health Check" + + System(AdminConsole, "Ed-Fi Admin Console", "A web application for managing ODS/API Deployments") + UpdateElementStyle(AdminConsole, $bgColor="silver") + + System_Boundary(backend, "Backend Systems") { + + Boundary(b0, "Admin API") { + Container(AdminAPI, "Ed-Fi Admin API 2") + + Container(HealthCheck, "Admin API Health
Check Worker") + } + + Boundary(b1, "ODS/API") { + System(OdsApi, "Ed-Fi ODS/API", "A REST API system for
educational data interoperability") + UpdateElementStyle(OdsApi, $bgColor="silver") + } + + Boundary(b2, "Shared Databases") { + ContainerDb(Admin, "EdFi_Admin,
EdFi_Security") + UpdateElementStyle(Admin, $bgColor="silver") + } + } + + Rel(AdminConsole, AdminAPI, "Issues HTTP requests") + + Rel(HealthCheck, AdminAPI, "Reads ODS/API connections,
Writes health info") + UpdateRelStyle(HealthCheck, AdminAPI, $offsetY="50") + + Rel(HealthCheck, OdsApi, "Reads records counts") + UpdateRelStyle(HealthCheck, OdsApi, $offsetX="-60", $offsetY="20") + + Rel(AdminAPI, Admin, "Reads and writes") + UpdateRelStyle(AdminAPI, Admin, $offsetY="50", $offsetX="10") + + UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="2") +``` + +## Solution Design + +### ODS/API Credentials + +The worker needs client credentials (also known as "key and secret") for +accessing each ODS Instance via the API. These credentials need to use their own +read only claimset providing access to all queries resources, using +`NoFurtherAuthorizationStrategy`. By implication then, the system must create +records in the following tables: + +1. `dbo.Vendor` creating an "Ed-Fi Administrative Tools" vendor with namespace + `uri://ed-fi.org`. +2. `dbo.Applications` creating an "Ed-Fi Health Check" application. +3. A new `dbo.ApiClients` and `dbo.ApiClientOdsInstances` record for each ODS + Instance. NOTE: while it is possible to associate a single `ApiClient` with + multiple `OdsInstances`, we prefer having a one-to-one relationship. + +#### Admin API's Responsibilities + +Managing these tables is Admin API 2's responsibility, not the health check +worker's responsibility. + +During _deployment_, Admin API 2 should: + +1. Create the vendor and application records. +2. Create the readonly claimset. + +The health check worker needs to retrieve client credentials from Admin API 2 +using `GET /adminconsole/instances`. A system administrator could modify the +`dbo.OdsInstance` and related tables outside of the Admin API 2 application. To +prevent errors, Admin API 2 should synchronize its own `adminconsole.Instances` +table with any new records in `dbo.OdsInstances` each time it starts. While +doing so, it can create client credentials for the Health Check Worker to access +that instance. + +The following diagram shows the startup synchronization process. When inserting +into `adminconsole.Instances`, set the status to "Completed". + +```mermaid +sequenceDiagram + AdminApi ->> EdFi_Admin: SELECT dbo.OdsInstances + AdminApi ->> EdFi_Admin: SELECT adminconsole.Instances + AdminApi ->> EdFi_Admin: SELECT dbo.Application WHERE name = Ed-Fi Health Check + + loop for each OdsInstance + AdminApi ->> AdminApi: Is OdsInstance in Instance list? + + opt no + AdminApi ->> EdFi_Admin: BEGIN TRANSACTION + AdminApi ->> EdFi_Admin: INSERT adminconsole.Instances + AdminApi ->> EdFi_Admin: INSERT dbo.ApiClients + AdminApi ->> EdFi_Admin: INSERT dbo.ApiClientOdsInstances + AdminApi ->> EdFi_Admin: UPDATE adminconsole.Instances (credentials) + AdminApi ->> EdFi_Admin: COMMIT + end + end +``` + +> [!TIP] +> Whenever the [Instance Management Worker](./INSTANCE-MANAGEMENT.md) creates a +> new ODS database instance, it must also create client credentials. This is shown +> in that document's sequence diagram. + +#### Encryption and Storage of Credentials + +The stored credentials must be encrypted at rest. When an Admin Console user +wants to review the status of the instances, the Admin Console application will +retrieve the instance information from Admin API 2 via `GET +/adminconsole/instances`. This request _must not_ contain the encrypted +credentials. The data used by the Admin Console are stored as a JSON object in +the `Document` column. The credentials should be stored in a new column: + +1. It is easier to avoid returning this information to the Admin Console since + it does not need to be removed from the `document` body. +2. Minimizing the amount of encrypted information will aid in application + debugging. + +Suggestion: add a new column `Credentials` of type string. Continue using JSON +storage, encrypting a string like the following: + +```json +{ + "client_id": "abcdedf", + "client_secret": "1232142" +} +``` + +The encryption can utilize the same key that Admin API 2 uses for encrypting +connection strings in `dbo.OdsInstances`. + +### Reading ODS/API Record Counts + +In this case, we will store the client/secret values in Admin API to call some +of the ODS/API endpoints to generate the returning payload to the Admin Console. +The payload contains a 'total-count' report of some of the resources, it takes +the value from the header in ODS/API. + +Example: + +```none +https://api.ed-fi.org:443/v7.1/api/data/v3/ed-fi/studentSchoolAssociations?limit=0&totalCount=true +``` + +The parameter `totalCount` is important to use because this will return us the +count in the header as `total-count`. With this value we can map it to our +payload in the field called `studentSchoolAssociations`. + +This process has to be called per field of the payload + +ODS/API resources to access: + +* studentSpecialEducationProgramAssociations +* studentDisciplineIncidentBehaviorAssociations +* studentSchoolAssociations +* studentSchoolAttendanceEvents +* studentSectionAssociations +* staffEducationOrganizationAssignmentAssociations +* staffSectionAssociations +* courseTranscripts +* sections + +> [!WARNING] Unknown fields +> +> * basicReportingPeriodAttendances +> * reportingPeriodExts +> * localEducationAgencyId: We are assuming this as Ed Org Id but we are not sure about this +> * healthy (boolean): We are asumming this as a flag that return true if the above data have been populated correctly and no error from ODS/API + +As we have to call multiple endpoints in this one, we are considering use a +caching approach (maybe the in-memory provided by .NET will be enough). If we +want to refresh the data we can send a flag to the endpoint to do so. + +### Storing Record Counts + +placeholder diff --git a/docs/design/adminconsole/INSTANCE-DATA.md b/docs/design/adminconsole/INSTANCE-DATA.md new file mode 100644 index 000000000..1314c1f9c --- /dev/null +++ b/docs/design/adminconsole/INSTANCE-DATA.md @@ -0,0 +1,418 @@ +# Instance Management Data + +See [APIs for Admin Console: Tenant +Management](./APIS-FOR-ADMIN-CONSOLE.md#tenant-management) for context. + +## REST Interface + +> [!NOTE] +> Admin Console might not be transmitting Tenant ID in the `POST` request +> as of Jan 20. We may need to modify Admin Console to include this. + +### GET /adminconsole/odsInstances + +Also supports `GET /adminconsole/odsInstances/{id}` + +* **Purpose**: Provide instance list to Admin Console. +* **Description**: + * Reads from the `adminconsole.Instance` table. + * No additional authorization required. + * The `baseUrl` value comes from the tenant information. + * Respond with 200 +* **Response Format**: + + ```json + [ + { + "odsInstanceId": 1, + "tenantId": 1, + "name": "Instance1", + "instanceType": "enterprise", + "baseUrl": "http://localhost/api", + "odsInstanceContexts": [ + { + "id": 1, + "odsInstanceId": 1, + "contextKey": "schoolYearFromRoute", + "contextValue": "2024" + } + ], + "odsInstanceDerivatives": [ + { + "id": 1, + "odsInstanceId": 2, + "derivativeType": "Read" + } + ] + } + ] + ``` + +### POST /adminconsole/odsInstances + +* **Purpose**: Accept instance creation requests from the Admin Console. +* **Description**: + * Validate the incoming payload. + * Insert data into the `adminconsole.Instance` table. + * Respond with `202 Accepted` and include the new id created. +* **Validation**: + + | Property | Rules | + | ------------------------------------ | ---------------------------------------------------------- | + | name | Max length: 100. Must be unique for the tenant. | + | instanceType | Max length: 100. | + | odsInstanceContexts | Empty array is allowed | + | odsInstanceContext.contextKey | Max length: 50. Must be unique for the instance. | + | odsInstanceDerivatives | Empty array is allowed | + | odsInstanceContext.contextValue | Max length: 50. | + | odsInstanceDerivative.derivativeType | Max length: 50. Allowed values: "ReadReplica", "Snapshot". | + +* **Sample Payload**: + + ```json + { + "odsInstanceId": 1, + "tenantId": 1, + "name": "Instance #1 - 2024", + "instanceType": "enterprise", + "odsInstanceContexts": [ + { + "contextKey": "schoolYearFromRoute", + "contextValue": "2024" + } + ], + "odsInstanceDerivatives": [ + { + "derivativeType": "ReadReplica" + } + ] + } + ``` + +* **Response Format**: + + ```json + { + "instanceId": 1, + } + ``` + +### PUT /adminconsole/odsInstances/{id} + +* **Purpose**: Update an instance definition. +* **Description**: + * Validate the incoming payload. + * Updates both the `adminconsole.Instance` and the `dbo.OdsInstances` tables. + * Respond with `202 No Content` + * If the `name` has changed: + * Set the status to `PENDING_RENAME` in the `adminconsole.Instance` table. + * Delete records from `dbo.OdsInstances` tables, so that the ODS/API does + not try to use this database while it is being renamed. +* **Validation**: + + | Property | Rules | + | ------------------------------------ | ---------------------------------------------------------- | + | name | Max length: 100. Must be unique for the tenant. | + | instanceType | Max length: 100. | + | odsInstanceContexts | Empty array is allowed | + | odsInstanceContext.contextKey | Max length: 50. Must be unique for the instance. | + | odsInstanceDerivatives | Empty array is allowed | + | odsInstanceContext.contextValue | Max length: 50. | + | odsInstanceDerivative.derivativeType | Max length: 50. Allowed values: "ReadReplica", "Snapshot". | + +* **Sample Payload**: + + ```json + { + "odsInstanceId": 1, + "tenantId": 1, + "name": "Instance #1 - 2024", + "instanceType": "enterprise", + "odsInstanceContexts": [ + { + "contextKey": "schoolYearFromRoute", + "contextValue": "2024" + } + ], + "odsInstanceDerivatives": [ + { + "derivativeType": "ReadReplica" + } + ] + } + ``` + +
+ +> [!IMPORTANT] +> The following diagram represents the possible values the Instances +> have during the Worker's process + +
+ +```mermaid +stateDiagram-v2 + classDef progressStyle font-style:italic,font-weight:bold,fill:#7a8eff + classDef noteStyle font-style:italic,font-weight:bold,fill:#ff9e89 + [*] --> Pending + state if_state_create <> + Pending --> InProgress + InProgress --> if_state_create + if_state_create --> Completed : Success + if_state_create --> InProgress : Retry + if_state_create --> CreateFailed : Error + CreateFailed --> Pending : Manually process + state Rename { + state if_state_rename <> + PendingRename --> RenameInProgress + RenameInProgress --> if_state_rename + if_state_rename --> RenameInProgress : Retry + if_state_rename --> RenameFailed : Error + RenameFailed --> PendingRename : Manually process + } + state Delete { + state if_state_delete <> + PendingDelete --> DeleteInProgress + DeleteInProgress --> if_state_delete + if_state_delete --> Deleted : Success + if_state_delete --> DeleteInProgress : Retry + if_state_delete --> DeletedFailed : Error + DeletedFailed --> PendingDelete : Manually process + Deleted --> [*] + } + if_state_rename --> Completed : Success + Completed --> PendingDelete + Completed --> PendingRename + Completed --> [*] + noteRetry : The retry mechanism will use the Polly library, after 3 retries the instance will set with the Failed status depending on the process (CreateFailed, RenameFailed, DeleteFailed). Once the Instance is set the Failed status, Admin Console will display an indicator telling the admin to check the log for more details + noteInProgress : *InProgress, DeleteInProgress, and RenameInProgress would be relevant in the multiple workers scenario + + class InProgress progressStyle + class DeleteInProgress progressStyle + class RenameInProgress progressStyle + class noteRetry noteStyle + class noteInProgress noteStyle +``` + +### DELETE /adminconsole/odsInstances/{id} + +* **Purpose**: Mark an instance for deletion. +* **Description**: + * Updates the status of the instance to `PENDING_DELETE`. + * This operation performs a soft delete by updating the status field; the record remains in the table. + * Does not immediately remove the instance; it is scheduled for deletion in other related systems. + * The instance must have a status of `COMPLETED` before it can be marked as `PENDING_DELETE`. + * Responds with `202 Accepted`. + +* **Validation**: + * The instance must exist. + * The current status must be `COMPLETED`. If not, the request is rejected with `409 Conflict`. + +* **Response Codes**: + * `202 Accepted` – The instance was successfully marked for deletion. + * `404 Not Found` – The specified instance does not exist. + * `409 Conflict` – The instance cannot be deleted because it is not in a `COMPLETED` state. + +### GET /adminconsole/instances + +Also supports `GET /adminconsole/instances/{id}` + +* **Purpose**: Provide instance list for the worker applications. +* **Description**: + * Reads from the `adminconsole.Instance` table. + * Must be authorized with an appropriate Role name in the token: `clientId` + and `clientSecret` must be left blank when this endpoint is accessed by an + Admin Console user. + * Returns a separate object for each ODS Instance Context. + * The `resourceUrl` is constructed from the tenant's base URL plus instance + context information. + * `odsInstanceId` will be null if the `status` is not `COMPLETED`. + * Return all values without the need for paging. + * Respond with 200 +* **Query String Parameters**: + * `status` to search by Status. + * `tenantName` to search by Tenant +* **Response Format**: + + ```json + [ + { + "tenantId": 1, + "tenantName": "Tenant1", + "instanceId": 1, + "odsInstanceId": 1, + "instanceName": "Instance #1 - 2024", + "resourceUrl": "http://localhost/api/2024/data/v3", + "oauthUrl": "http://localhost/api/2024/oauth/token", + "clientId": "abc123", + "clientSecret": "d5rftyguht67gyhuijk", + "status": "Completed" + } + ] + ``` + +> [!NOTE] +> In the future the health check worker should use the read replica / snapshot +> if available. For now, it will use the primary database instance. + +### POST /adminconsole/instances/{id}/completed + +* **Purpose**: Updates the given `adminconsole.Instance` record by changing the + status to `COMPLETED`. +* **Description**: + * Responds with `204 No Content` if the record is _already complete_ or if the operations described below succeed. + * Responds with `404 Not Found` if the Id does not exist. + * As described in [Instance Management Worker](./INSTANCE-MANAGEMENT.md), this + action does the following work if the status is not already `COMPLETED`, + using a single database transaction: + * Insert into `dbo.OdsInstances`. + * If needed, insert into `dbo.OdsInstanceContexts` and `dbo.OdsInstanceDerivatives`. + * Insert into `dbo.ApiClients` to create credentials for the [Health Check Worker](./HEALTH-CHECK-WORKER.md). + * Insert into `dbo.ApiClientOdsInstances`. + * Update `adminconsole.Instance` to set: + * New credentials + * Status = `COMPLETED` +* **Validation**: + + | Property | Rules | + | ------------------------------------ | ---------------------------------------------------------- | + | connectionString | Valid mssql or pgsql connection string. | + +* **Sample Payload**: + + ```json + { + "connectionString": "host=localhost;port=5431;username=username;password=password;database=database;application name=AppName;" + } + ``` + +### PUT /adminconsole/instances/{id} + +* Not supported at this time. Respond with `405 Method Not Allowed`. + +### DELETE /adminconsole/instances/{id} + +* Not supported at this time. Respond with `405 Method Not Allowed`. + +### POST /adminconsole/instances/{id}/deleted + +* **Purpose**: Marks the given `adminconsole.Instance` record as `DELETED`. +* **Description**: + * Responds with `204 No Content` if the record is already marked as "Deleted" or if the operations described below succeed. + * Responds with `404 Not Found` if the specified ID does not exist. + * As described in [Instance Management Worker](./INSTANCE-MANAGEMENT.md), this action performs the following operations in a single database transaction if the status is not already `DELETED`: + * Delete the corresponding record from `dbo.OdsInstances`. + * If applicable, delete related records from `dbo.OdsInstanceContexts` and `dbo.OdsInstanceDerivatives`. + * Delete associated records from `dbo.ApiClients` and `dbo.ApiClientOdsInstances`. + * Update the `adminconsole.Instance` record to: + * **Status** = `DELETED`. + +### POST /adminconsole/instances/{id}/deleteFailed + +* **Purpose**: Marks the given `adminconsole.Instance` record as `DELETE_FAILED` if the database drop operation performed by the Instance Management Worker has failed. +* **Description**: + * Responds with `204 No Content` if the record is already marked as `DELETE_FAILED` or if the operation succeeds. + * Responds with `404 Not Found` if the specified ID does not exist. + * As described in [Instance Management Worker](./INSTANCE-MANAGEMENT.md), this action updates the `adminconsole.Instance` record to: + * **Status** = `DELETE_FAILED`. + +### POST /adminconsole/instances/{id}/renamed + +* **Purpose**: Marks the given `adminconsole.Instance` record as `COMPLETED`. +* **Description**: + * Responds with `204 No Content` if the record is already marked as `COMPLETED` or if the operations described below succeed. + * Responds with `404 Not Found` if the specified ID does not exist. + * As described in [Instance Management Worker](./INSTANCE-MANAGEMENT.md), this action performs the following operations in a single database transaction if the status is not already `COMPLETED`: + * Update the name and connection string + * Re-insert into the `dbo.OdsInstances` tables and create new Health Check Worker credentials following the procedure used for `POST /adminconsole/instances/{id}/completed`. + * Set `adminconsole.Instance.Status='COMPLETED'`. + +### POST /adminconsole/instances/{id}/renameFailed + +* **Purpose**: Marks the given `adminconsole.Instance` record as `RENAME_FAILED` if the database drop operation performed by the Instance Management Worker has failed. +* **Description**: + * Responds with `204 No Content` if the record is already marked as `RENAME_FAILED` or if the operation succeeds. + * Responds with `404 Not Found` if the specified ID does not exist. + * As described in [Instance Management Worker](./INSTANCE-MANAGEMENT.md), this action sets `adminconsole.Instance.Status='RENAME_FAILED'`. + +## Future + +These endpoints will not be supported in Admin API 2.3, but are under consideration for a future version. + +### POST /adminconsole/instances/jobs/start + +* **Purpose**: Start processing jobs from the `adminconsole.Instance` table. +* **Description**: + * Select rows with any of these conditions: + * Status is "Pending" and `lockDateTime is null`. + * Status is "In Progress" and `lockDateTime` is expired (expiration timeout + value to be set in appsettings, for example 60 minutes). _This provides an + automated retry process_ + * Lock rows in the `adminconsole.Instance` table for processing by setting a + `jobId` value (UUID) and setting column `lockDateTime` to "now". + * Changes the status to `In Progress` + * Responds with `200 OK`. +* **Response Format**: + + ```json + { + "jobId": "", + "instances": [ + { + "odsInstanceId": 1, + "tenantId": 1, + "name": "Instance #1 - 2024", + "instanceType": "enterprise", + "odsInstanceContexts": [ + { + "contextKey": "schoolYearFromRoute", + "contextValue": "2024" + } + ], + "odsInstanceDerivatives": [ + { + "derivativeType": "ReadReplica" + } + ] + } + ] + } + ``` + +### POST /adminconsole/instances/jobs/{id}/completed + +* **Purpose**: Mark a job as complete and perform transactional updates. +* **Enhancements**: + * Accept a job completion payload. + * Add resultant data to tables `OdsInstances`, `OdsInstanceContext`, `OdsInstanceDerivatives` and update `adminconsole.Instance` status column to mark job as `Compelete` within a single transaction. + * Roll back on failure. + * Respond with `200 Ok`. + +## Data Storage + +No modifications will be made in the `dbo.*` tables. + +### adminconsole.Instance + +In the normal flow of work, this table will be populated by Admin Console / +Admin API _before_ a matching record exists in the `dbo.OdsInstances` table. The +`Document` column shown below is a flexible JSON object to whatever information +is necessary to support both the user interface and the creation of records in +`dbo.OdsInstances`, `dbo.OdsInstanceContext`, and `dbo.OdsInstanceDerivatives`. +The JSON structure gives the team flexibility for rapid iteration. + +Columns that need to be indexed (e.g. `status`) or updated by worker processes +should be real columns, instead of embedding them in the JSON data. + +| Column Name | Type | Nullable | Purpose | +| ------------- | -------------- | -------- | ------------------------------------------------------------ | +| InstanceId | int | no | Auto-incrementing identifier | +| OdsInstanceId | int | yes | Matching value from `dbo.OdsInstances` | +| TenantId | int | no | Tenant identifier | +| Document | JSON / string | yes | JSON document containing all but credentials information | +| Credentials | varbinary(500) | no | Encrypted JSON document with `client_id` and `client_secret` | +| Status | string | no | Pending, Completed, InProgress, or Error | +| CompletedAt | datetime | yes | Set this value when completed | + +> [!NOTE] +> Is `varbinary(500)` sufficient to hold encrypted credentials? diff --git a/docs/design/adminconsole/INSTANCE-MANAGEMENT.md b/docs/design/adminconsole/INSTANCE-MANAGEMENT.md new file mode 100644 index 000000000..9d523dacc --- /dev/null +++ b/docs/design/adminconsole/INSTANCE-MANAGEMENT.md @@ -0,0 +1,356 @@ +# Instance Management + +This document describes the work performed by the Admin API 2 application and +its associated Instance Management Worker for creating or deleting database +instances. Additional features and technical requirements are in the [Instance +Management +Worker](https://github.com/Ed-Fi-Alliance-OSS/Ed-Fi-Admin-Console-Instance-Management-Worker-Process/blob/main/docs/INSTANCE-MANAGEMENT.md) +repository. The document below describes Admin API's role in support of the +worker process. + +## Containers + +```mermaid +C4Container + title "Instance Management" + + System(AdminConsole, "Ed-Fi Admin Console", "A web application for managing ODS/API Deployments") + UpdateElementStyle(AdminConsole, $bgColor="silver") + + System_Boundary(backend, "Backend Systems") { + + Boundary(b0, "Admin API") { + Container(AdminAPI, "Ed-Fi Admin API 2") + Container(Instance, "Admin API Instance
Management Worker") + } + + Boundary(b1, "ODS/API") { + System(OdsApi, "Ed-Fi ODS/API", "A REST API system for
educational data interoperability") + UpdateElementStyle(OdsApi, $bgColor="silver") + + SystemDb(ods3, "EdFi_ODS_") + } + + Boundary(b2, "Shared Databases") { + ContainerDb(Admin, "EdFi_Admin,
EdFi_Security") + } + } + + Rel(AdminConsole, AdminAPI, "Issues HTTP requests") + + Rel(Instance, AdminAPI, "Reads instance requests,
Write instance status") + UpdateRelStyle(Instance, AdminAPI, $offsetY="50", $offsetX="-10") + + Rel(Instance, ods3, "Creates new ODS instances") + UpdateRelStyle(Instance, ods3, $offsetY="20", $offsetX="-50") + + Rel(OdsApi, ods3, "Reads and writes") + UpdateRelStyle(OdsApi, ods3, $offsetX="10") + + Rel(AdminAPI, Admin, "Reads and writes") + UpdateRelStyle(AdminAPI, Admin, $offsetY="50", $offsetX="10") + + Rel(OdsApi, Admin, "Reads") + UpdateRelStyle(OdsApi, Admin, $offsetY="20", $offsetX="-10") + + UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="2") +``` + +## Functional Requirements + +Users will need the ability to perform the following operations for ODS database +instances: + +1. Create a new instance and insert records into the `dbo.OdsInstances` and related tables. +2. Rename an existing instance and update the records (including connection string) in `dbo.OdsInstances`. +3. Delete an existing instance and delete records from `dbo.OdsInstances`. +4. Backup an existing instance +5. Restore an existing instance from backup + +The first three operations will also require updating the `Status` field in the +`adminconsole.Instance` table. From this perspective, the delete operation will +be a _soft delete_ for audit purposes. That is, the +`adminconsole.Instance.Status` field will be set to "DELETED" instead of +physically deleting the row. + +## Solution Design + +The Instance Management Worker will be a command line interface (CLI) +application that should run on a schedule, e.g. as a Cron job in Linux or a +Windows Scheduled Task. The Worker's responsibility is to create and manage +ODS database instances ("CREATE DATABASE"). It will pull task information from +Admin API 2, and create databases directly in the RDBMS. It writes status back +to Admin API 2 so that it can update the instance management tables used by the +ODS/API (e.g. `dbo.OdsInstances`). + +Ultimately, the solution needs to be robust for error handling, retries, and for +extensibility to support cloud-based platforms. These functions will be built +out incrementally as needed based on feedback from the field: + +1. Assume single worker running at a time; fail gracefully; require manual + intervention when errors occur. Support for "on prem" type of connectivity + with PostgreSQL and MSSQL. Single Tenant. +2. Multi tenancy - creating additional Admin and Security databases. +3. More robust job handling for concurrent execution. +4. Infrastructure for Cloud managed databases (e.g. AWS Aurora, AWS RDS, + Azure Cosmos DB, Azure SQL Server, Azure PostgreSQL). + +> [!TIP] +> The processes below refer to a new `Instances` table managed by Admin API 2. +> Admin API 2 on startup queries the `dbo.OdsInstances` table used by the ODS/API +> and inserts missing records into the new table. This solves a potential +> synchronization problem between these two tables. + +### v1: Single Worker + +### New Instance + +```mermaid +sequenceDiagram + actor Console as Admin Console + actor Worker as Instance Management Worker + participant AdminAPI + participant EdFi_Admin + + Console ->> AdminAPI: POST /adminconsole/odsInstances + AdminAPI ->> EdFi_Admin: INSERT INTO adminconsole.Instance + AdminAPI -->> Console: 202 Accepted + + Worker ->> AdminAPI: GET /adminconsole/instances?status=pending + AdminAPI -->> Worker: instances + + loop For Each Pending Instance + create participant DbServer + + Worker ->> DbServer: create / copy database from template + + Worker ->> AdminAPI: POST /adminconsole/instances/{id}/completed + + AdminAPI ->> EdFi_Admin: BEGIN TRANSACTION + AdminAPI ->> EdFi_Admin: INSERT INTO dbo.OdsInstances + AdminAPI ->> EdFi_Admin: INSERT INTO dbo.OdsInstanceContext + AdminAPI ->> EdFi_Admin: INSERT INTO dbo.OdsInstanceDerivative + + note right of EdFi_Admin: Credentials for Health Check Worker + rect rgb(191, 223, 255) + AdminAPI --> EdFi_Admin: INSERT INTO dbo.ApiClients + AdminAPI --> EdFi_Admin: INSERT INTO dbo.ApiClientOdsInstances + end + + AdminAPI ->> EdFi_Admin: UPDATE adminconsole.Instances (status, credentials) + + AdminAPI ->> EdFi_Admin: COMMIT + + end + + AdminAPI -->> Worker: 200 OK +``` + +### Delete Instance + +```mermaid +sequenceDiagram + actor Console + actor Worker + participant AdminAPI + participant EdFi_Admin + participant DbServer + + Console ->> AdminAPI: DELETE /adminconsole/odsInstances/{id} + AdminAPI ->> EdFi_Admin: UPDATE adminconsole.Instance SET status = "PENDING_DELETE" + AdminAPI -->> Console: 204 Ok + + Worker ->> AdminAPI: GET /adminconsole/instances?status=PENDING_DELETE + AdminAPI ->> EdFi_Admin: Fetch instances with status=PENDING_DELETE + EdFi_Admin -->> AdminAPI: List of instances + + AdminAPI -->> Worker: List of instances + + loop For each instance in list + Worker ->> DbServer: Drop database + + alt Drop successful + Worker ->> AdminAPI: POST /adminconsole/instances/{id}/deleted + + AdminAPI ->> EdFi_Admin: BEGIN TRANSACTION + AdminAPI --> EdFi_Admin: UPDATE Status = DELETED FROM adminconsole.Instance Status + AdminAPI ->> EdFi_Admin: DELETE FROM dbo.OdsInstanceDerivative + AdminAPI ->> EdFi_Admin: DELETE FROM dbo.OdsInstanceContext + AdminAPI ->> EdFi_Admin: DELETE FROM dbo.OdsInstances + AdminAPI ->> EdFi_Admin: DELETE FROM dbo.ApiClients and dbo.ApiClientOdsInstances + AdminAPI ->> EdFi_Admin: COMMIT TRANSACTION + + AdminAPI -->> Worker: 204 OK + else Drop failed + Worker ->> AdminAPI: POST /adminconsole/instances/{id}/deleteFailed + AdminAPI --> EdFi_Admin: UPDATE Status = DELETE_FAILED FROM adminconsole.Instance Status + end + end +``` + +### Rename Instance + +```mermaid +sequenceDiagram + actor Console + actor Worker + participant AdminAPI + participant EdFi_Admin + participant DbServer + + Console ->> AdminAPI: DELETE /adminconsole/odsInstances/{id} + AdminAPI ->> EdFi_Admin: UPDATE adminconsole.Instance SET status = "PENDING_RENAME" + AdminAPI -->> Console: 204 Ok + + Worker ->> AdminAPI: GET /adminconsole/instances?status=PENDING_RENAME + AdminAPI ->> EdFi_Admin: Fetch instances with status=PENDING_RENAME + EdFi_Admin -->> AdminAPI: List of instances + + AdminAPI -->> Worker: List of instances + + loop For each instance in list + Worker ->> DbServer: Rename database + + alt Drop successful + Worker ->> AdminAPI: POST /adminconsole/instances/{id}/renamed + + AdminAPI ->> EdFi_Admin: BEGIN TRANSACTION + AdminAPI ->> EdFi_Admin: INSERT INTO dbo.OdsInstances + AdminAPI ->> EdFi_Admin: INSERT INTO dbo.OdsInstanceContext + AdminAPI ->> EdFi_Admin: INSERT INTO dbo.OdsInstanceDerivative + + note right of EdFi_Admin: Credentials for Health Check Worker + rect rgb(191, 223, 255) + AdminAPI --> EdFi_Admin: INSERT INTO dbo.ApiClients + AdminAPI --> EdFi_Admin: INSERT INTO dbo.ApiClientOdsInstances + end + + AdminAPI ->> EdFi_Admin: UPDATE adminconsole.Instances (status, credentials) + + AdminAPI -->> Worker: 204 OK + else Rename failed + Worker ->> AdminAPI: POST /adminconsole/instances/{id}/renameFailed + AdminAPI --> EdFi_Admin: UPDATE Status = RENAME_FAILED FROM adminconsole.Instance Status + end + end +``` + +### Backup Instance + +TBD + +### Restore Backup + +TBD + +#### Health Check Client Credentials + +The section in blue is in support of the Health Check Worker, which needs client +credentials for accessing the newly created instance. Admin API 2 will need to +create and store new credentials each time an instance is created. Note that the +credentials are stored in a separate column from the rest of the document +information, and they should be encrypted. For more information, see [Health +Check Worker: Admin API's +Responsibilities](./HEALTH-CHECK-WORKER.md#admin-apis-responsibilities). + +### v2: Multi Tenancy + +For multi-tenancy support, the Instance Management Worker must also create new +instances of the `EdFi_Admin` and `EdFi_Security` databases. The Admin API +`Instances` table will still be in the original `EdFi_Admin` database. + +> [!NOTE] +> Community feedback may suggest creating a single new multi-tenant database +> that is independent of any given `EdFi_Admin` instance / tenant. For now, the +> simplest path forward is to use the first instance. + +```mermaid +sequenceDiagram + actor Console as Admin Console + actor Worker as Instance Management Worker + participant AdminAPI + participant EdFi_Admin as EdFi_Admin DB + participant DbServer + participant EdFi_Admin_New as EdFi_Admin_ + + Console ->> AdminAPI: POST /adminconsole/instances + AdminAPI ->> EdFi_Admin: INSERT INTO adminconsole.Instance + AdminAPI -->> Console: 202 Accepted + + Worker ->> AdminAPI: GET /adminconsole/instances?status=pending + AdminAPI -->> Worker: instances + + loop For Each Pending Instance + + Worker ->> DbServer: create / copy ODS database from template + Worker ->> DbServer: create / copy Admin database from template + Worker ->> DbServer: create / copy Security database from template + + Worker ->> AdminAPI: POST /adminconsole/instances/{id}/created + + AdminAPI ->> EdFi_Admin_New: BEGIN TRANSACTION + AdminAPI ->> EdFi_Admin_New: INSERT INTO dbo.OdsInstances + AdminAPI ->> EdFi_Admin_New: INSERT INTO dbo.OdsInstanceContext + AdminAPI ->> EdFi_Admin_New: INSERT INTO dbo.OdsInstanceDerivative + + note right of EdFi_Admin: Credentials for Health Check Worker + rect rgb(191, 223, 255) + AdminAPI --> EdFi_Admin_New: INSERT INTO dbo.ApiClients + AdminAPI --> EdFi_Admin_New: INSERT INTO dbo.ApiClientOdsInstances + end + + AdminAPI ->> EdFi_Admin: UPDATE adminconsole.Instance (status, credentials) + + AdminAPI ->> EdFi_Admin: COMMIT + + end + + AdminAPI -->> Worker: 200 OK +``` + +### v3: Concurrency + +The following diagram only displays the single tenant situation, but is easily +adapted to multi-tenancy. + +```mermaid +sequenceDiagram + actor Console + actor Worker + participant AdminAPI + participant EdFi_Admin + participant DbServer + + Console ->> AdminAPI: POST /adminconsole/instances + AdminAPI ->> EdFi_Admin: INSERT INTO adminconsole.Instance + AdminAPI -->> Console: 202 Accepted + + Worker ->> AdminAPI: POST /adminconsole/instances/jobs/start + AdminAPI ->> EdFi_Admin: Set lock and return rows from Instance + EdFi_Admin -->> AdminAPI: locked rows + + AdminAPI -->> Worker: job information with job id + + Worker ->> Worker: createDatabase + Worker ->> DbServer: create / copy database from template + + Worker ->> AdminAPI: POST /adminconsole/instances/jobs/{id}/complete + + AdminAPI ->> EdFi_Admin: BEGIN TRANSACTION + AdminAPI ->> EdFi_Admin: INSERT INTO dbo.OdsInstances + AdminAPI ->> EdFi_Admin: INSERT INTO dbo.OdsInstanceContext + AdminAPI ->> EdFi_Admin: INSERT INTO dbo.OdsInstanceDerivative + AdminAPI ->> EdFi_Admin: INSERT key & secret INTO dbo.Application + + AdminAPI ->> EdFi_Admin: UPDATE adminconsole.Instance.Document including key & secret + + AdminAPI ->> EdFi_Admin: COMMIT + + AdminAPI -->> Worker: 200 OK +``` + +### v4: Cloud Support + +> [!NOTE] +> Placeholder. Assuming that the create database process will differ across the +> managed database solutions. \ No newline at end of file diff --git a/docs/design/adminconsole/SQL-SERVER-SUPPORT.md b/docs/design/adminconsole/SQL-SERVER-SUPPORT.md new file mode 100644 index 000000000..7b377c220 --- /dev/null +++ b/docs/design/adminconsole/SQL-SERVER-SUPPORT.md @@ -0,0 +1,231 @@ +# Adding MS SQL Support to Instance Management + +## Summary + +The Instance Management worker allows system administrators to manage instances of their ODS. This worker is currently configured to support PostgreSQL and needs support added for SQL Server. + +## Background + +The Instance Management worker was designed to execute ODS administration tasks without tying up a handler's main process. This asynchronous nature requires the Instance management worker to manage other aspects of its environment, including persistence and compatibility with other services. The data layer for the Instance Management worker was implemented using PostgreSQL, with the idea of using this dialect of SQL to start, verifying its implementation, then including support for SQL Server after. + +The Postgres implementation of the Instance Manager includes support for Creating, Renaming, Deleting, and Restoring ODS Instances. + +There is a Postgres DB dump of sample student data that serves as a starting point for minimal and populated templates. No such sample exists for SQL Server so this will need to be solved in order to remain in parity with the Postgres provisioning features. Additionally, SQL Server utilizes a slightly different dialect from server so these actions must be converted. Lastly, SQL Server licensing requires that images / containers including the Azure SQL Server base image not be distributed. This does not prohibit providing instructions on how to build these images. + +Azure SQL Server support is an additional consideration but will remain out of the scope of this implementation. + +## Design Overview + +Adding support for SQL Server has been divided into the following four distinct areas of focus below. + +### 1. Configuring and connecting to the database + +The first step involves adding a connection and corresponding configuration to the application. This step is to ensure the application is communicating with the desired SQL Server instance (Platform hosted, docker hosted). There is a `CreateConnection()` method in the [`SqlServerSandboxProvisioner.cs`](https://github.com/Ed-Fi-Alliance-OSS/Ed-Fi-ODS/blob/main/Application/EdFi.Ods.Sandbox/Provisioners/SqlServerSandboxProvisioner.cs) example from ODS Sandbox, which demonstrates how to connect properly. + +### 2. Translating the actions to MS SQL dialect for each of the admin functions + +The current Postgres implementation of the Instance Management Worker borrows from the provisioner patterns seen in the `EdFi.Ods.Sandbox` project. + +The `PostgresSandboxProvisioner.cs` class contains methods for creating connections, renaming, deleting, managing file paths, retrieving DB Status among others. These actions can be used to inform the implementation details for the Instance Management worker and the corresponding SQL Server actions. These actions will need to be translated to MS SQL and added to a new`SqlServerInstanceProvisioner.cs` file located in the `Provisioners/` directory. + +Note, for restoring the database, the SqlServerSandboxProvisioner.cs from the API Sandbox uses a `CopySandboxAsync` command which sets up internal functions to backup a target and restore to a new destination. An internal function in particular, `GetBackupDirectoryAsync` uses the windows registry to locate the backup directory of the server. This will need to change, as SQL Server can run on platforms outside of Windows. The design details will configures a backup location the host and the application can read. + +Other supporting configuration files, e.g. `Provisioners/DatabaseEngineProvider.cs` will also need to be updated to reflect added support for SQL Server, resulting in a seamless experience between selecting the PostgreSQL engine and SQL Server. + +### 3. Low-friction SQL Server connection setup + +The next task is providing a low-friction environment for users to spin up and connect to the desired SQL Server instance. Historically, Ed-Fi has provided guidance to users on how to provision SQL Server configurations for various environments. This is done to avoid hosting images containing the distribution itself, which would create conflict with the Apache 2.0 license that accompanies the Ed-Fi Alliance source code and tools. + +SQL Server Options that Ed-Fi provides guidance: + +1. Installation included with official binaries +2. Experimental, bare MSSQL install scripts +3. Docker compose with local sample data (Users SQL Server Express Edition) + +A quick reference for setting up SQL Server runtime can be found at the following [tech docs guide](https://docs.ed-fi.org/reference/docker/#step-2-setup-runtime-environment). Because the Instance Management worker already takes advantage of Docker compose, option three appears to provide the most benefit for little effort. This will still allow users to get up and running quickly without spending much time provisioning a host machine. + +### 4. Seeding data for restoration + +Lastly, the Instance Management worker should be able to restore an ODS instance. This by extension means that the worker must support exporting the data, creating the instance, and importing the data to a created instance. + +The SQL Server database will need to be populated in order to provide the necessary data for export. Two viable options to consider for migrating the data include: + +1. Creating the Sample data and tables directly using synthesized data. +2. Transforming Sample data from PostgreSQL backups + +It appears that the MSSQL Sandbox compose files include a populated template that connects to a predefined volume. An approach can be to connect to this instance then export the to a BACPAC file, which can be used for restoration. + +Once this is done successfully, the next step is to implement the restoration of this BACPAC template data. An action can be added to the interface that runs the necessary steps within the transaction that results in creating an instance and subsequently reading the BACPAC. + +## Design Details + +The following adds additional implementation details on the design overview provided above. + +### 1. Configuring and connecting to the Database + +This step ensure that a Database connection can be established using the provided connection string settings. + +There is an `InstanceProvisionerBase.cs` class that is responsible for retrieving the connection strings and configuration settings when the provisioner is instantiated. This class also set up abstractions for creating a DbConnection, which can be overwritten for use in the SQL Engine specific Provisioner. The following is used to create a PostgreSQL DB connection: + +```csharp +protected override DbConnection CreateConnection() => new NpgsqlConnection(ConnectionString); +``` + +The `CreateConnection()` method is then used throughout the Provisioner to create a connection object for executing queries against. + +```csharp +using (var conn = CreateConnection()) +//...remaining implementation with conn. +``` + +The _databaseNameBuilder.SandboxNameForKey(clientKey) will likely need to be modified with multi-tenancy under consideration. + +### 2. Translate actions to MS SQL Dialect + +The next lift is to translate each of the DB Provisioning methods to the appropriate MS SQL Dialect + +#### Create + +The PostgreSQL provisioner implementation of `CopyDbInstanceAsync` container includes a `CREATE` statement that uses a `TEMPLATE` database. The SQL Server implementation does not support Db templates, so this will need to user a bak file to configure the populated template. This bak file will be restored when a Create DB instance command is issued. + +The Admin API Sandbox has SqlServerSandboxProvisioner.cs which implements a method `CopySandboxAsync`. This method illustrates the restoration method used with SQL Server and the backup file. This excerpt summarizes the process by calling internal functions near the top, making it easier to follow along. + +```csharp + using (var conn = CreateConnection()) + { + try + { + await conn.OpenAsync(); + + // This points to where the template should be saved + string backupDirectory = await GetBackupDirectoryAsync() + .ConfigureAwait(false); + + _logger.Debug($"backup directory = {backupDirectory}"); + + string backup = await PathCombine(backupDirectory, originalDatabaseName + ".bak"); + _logger.Debug($"backup file = {backup}"); + + var sqlFileInfo = await GetDatabaseFilesAsync(originalDatabaseName, newDatabaseName) + .ConfigureAwait(false); + + await BackUpAndRestoreSandbox() + .ConfigureAwait(false); + //.......... + } + // ........ + } +``` + +Note that the `GetBackupDirectoryAsync` will need to use a value other than the `HKEY_LOCAL_MACHINE` for retrieving the backup directory location. This could be replaced with an AppSettings configuration and will need to be configurable when using the RESTORE functionality. + +#### Delete + +Deleting instances from the Management Worker context requires removing the Database and tables associated with the client key. For reference, the Admin API Sandbox does the following: + +```csharp + using (var conn = CreateConnection()) + { + foreach (string key in deletedClientKeys) + { + await conn.ExecuteAsync($@" + IF EXISTS (SELECT name from sys.databases WHERE (name = '{_databaseNameBuilder.SandboxNameForKey(key)}')) + BEGIN + ALTER DATABASE [{_databaseNameBuilder.SandboxNameForKey(key)}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + DROP DATABASE [{_databaseNameBuilder.SandboxNameForKey(key)}]; + END; + ", commandTimeout: CommandTimeout) + .ConfigureAwait(false); + } + } +``` + +The above is used to iterate through DB associated with the client key to perform the drop. + +#### Rename + +Renaming an instance is a two part process. The first is renaming the instances and tables themselves. This can be done with an ALTER command similar to this: + +```csharp +await conn.ExecuteAsync($"ALTER DATABASE {oldName} MODIFY Name = {newName};") + .ConfigureAwait(false); +``` + +The next step is updating data to reference this new instance. The client credentials and secrets table will need to be updated - care must be exercised to ensure renaming a table doest not impact the effectiveness of these credentials. + +Lastly, the system will need a fallback for handling potential reads while renaming the system. One approach is to spin up the new instance while the old is still running, then switch the configuration once the new instance has been set up. This will help mitigate downtime, but coordinating this will add complexity and potentially require more resources. + +Another approach is to simple take down the existing instance, rename and wait until the system is available again. This requires less complexity to manage but does increase downtime. + +Deciding on a best approach will depending on what level of unavailability is acceptable. + +#### Restore + +The Restoration will behave similar to the create process. The main difference is the restore will target a custom .bak file provided by a user, while the create will use a predefined .bak that will scaffold necessary tables and data. + +The configuration used to set the backup path in AppSettings can be used to locate the .bak file the user would like restored. The restoration then becomes similar to the create, but with a targeted file to restore. + +### 3. Low-friction SQL Server Setup + +We will explore using the Docker Compose environment for configuring and setting up the SQL Server and connecting with the Admin API. This will require us to configure the SQL Server container and network settings in a way that works with the Admin API but also portable for easy for developers. + +#### Build SQL Server compatible image + +Ensure we can configure and run an SQL Server image with the right requirements. + +#### Start up compose network + +Reference the build file to create an image and configure compose network. This allows compose to con3figure the runtime settings of the SQL Server container. + +#### Ensure running and connect directly via Docker host + +Once the container is running using compose, confirm the services are behaving as expected by connecting via the Docker host. This might involve using the host machine to connect to the shell of the SQL Server container. + +#### Pass connection settings to connect via application + +Once the container has been verified, derive the connection string and pass to the application to ensure connection to SQL Server container is valid. + +### 4. Seeding Data for restoration + +This last section involves provisioning the Template data that will be used during the CREATE instance action. We will need the schema, tables, roles and DB set up so that the necessary configuration is provided when a user requests a new instance. + +## Test Strategy + +The following user journeys represent areas critical to instance management using SQL Server. + +An admin can connect to a SQL Server. + +* Provision the server +* Create connection string +* Ensure that the application connect to server successfully + +An admin can add a new SQL Server ODS instance + +* Connect to the SQL Server Instance +* Execute command to create a new DB Instance and tables +* Demonstrate new instance and tables are available + +An admin can rename an existing SQL Server ODS instance + +* Connect to the SQL Server Instance +* Execute command to rename ODS instance +* Ensure DB and corresponding tables and connection strings are updated +* Add additional path checking duplicate name for rename does not exist + +An admin delete a new SQL Server ODS instance + +* Connect to the SQL Server instance +* Execute command to delete instance +* Ensure instance and corresponding tables are properly marked for deletion. + +An admin can restore a new SQL Server ODS instance + +* Connect to the SQL Server instance +* Execute the create command providing a name and source of the restoration +* Ensure a new instance exists with provided restoration data (dbs, tables, and rows) + +## Outstanding Questions + +When renaming and instance, what is the expectation around down time? + +When searching for an instance to rename or delete by key, is it possible for a client key to reference more than one ODS instance? diff --git a/docs/design/adminconsole/TENANT-DATA.md b/docs/design/adminconsole/TENANT-DATA.md new file mode 100644 index 000000000..d37b3b3ed --- /dev/null +++ b/docs/design/adminconsole/TENANT-DATA.md @@ -0,0 +1,37 @@ +# Tenant Management Data + +See [APIs for Admin Console: Tenant +Management](./APIS-FOR-ADMIN-CONSOLE.md#tenant-management) for context. + +> [!NOTE] +> Full CRUD support with database backend will be covered in a future version. + +## REST Interface + +### GET /adminconsole/tenants + +Also supports `GET /adminconsole/tenants/{id}` + +* **Purpose**: Provide tenant list to Admin Console. +* **Description**: + * Reads from the appsettings file. + * No additional authorization required. + * Respond with 200 +* **Response Format**: + + ```json + [ + { + "tenantId": 1, + "document": { + "name": "Tenant1", + "discoveryApiUrl": "http://localhost/api" + } + } + ] + ``` + +## Data Storage + +The required values will initially be stored in the appSettings file instead of +a database table. diff --git a/docs/design/adminconsole/WORKER-CRON-PROCEES.md b/docs/design/adminconsole/WORKER-CRON-PROCEES.md new file mode 100644 index 000000000..45472781b --- /dev/null +++ b/docs/design/adminconsole/WORKER-CRON-PROCEES.md @@ -0,0 +1,202 @@ +# Running Cron process inside of Docker container + +___ + +## Summary + +This technical design proposes a solution for running the Health worker and Instance worker on a schedule using the `cron` process. The schedule is configurable using a `crontab` file and allows the workers to log and execute in specified intervals, all from within a container. + +## Background + +The Admin Console is a web client that provides an interface for users and admins to manage instances. In order to provide a quality experience in the console app, instance health and status collection is delegated to specialized service workers. + +The [Health Check Worker](./HEALTH-CHECK-WORKER.md) requires a periodic update on the health status, to alert the user if an instance is unreachable or in a failure state. + +The [Instance Management Worker](./INSTANCE-MANAGEMENT.md) registers and manages instances from a connected ODS/API + +For the purpose of the design, we assume that each of these workers can publish a production ready executable that, once running, calls a series endpoints defined in the execution environment. + +## Design Overview + +This design starts by stating clear assumptions and considerations about the problem statement and environment when determining the solution. + +A recommendation follows, outlining suggested steps for running the workers on a schedule in a container environment using crontab. This involves modifying a Docker file to support cron, as well as adding logs and environment variables to inject into the container. + +Following the recommendation is a testing strategy for ensuring the crontab task is running and logging. + +## Design Details + +### Assumptions + +In order to scope the investigation, the following assumptions about the user's environment were made: + +* The parameters needed by the executable are configured to be read from the execution environment. +* The user has access to the infrastructure needed to support execution of the application using the configured address. +* We assume the application is meant to deploy in a container environment, levering docker-compose for debugging and development. + +### Considerations + +When determining a solution, the following approaches were considered. + +* Create the schedule in the executable itself and copy into image +* Control schedule using external host / orchestrator +* Managing the schedule within the Docker Image itself + +Below are quick notes on each approach and the recommended approach. + +#### Schedule within the executable + +Scheduling within the executable means managing the schedule from within the application itself. While this makes setup locally simple, this approach that has some immediate obvious downsides. The application owner must manage the scheduler and must do so within the execution context of the application. Also, the container would have to run continuously, so the user would need to exercise care managing resources for a long running application process. + +#### Schedule controlled by the host system / orchestrator + +This approach is more common in container orchestration environments where a control plane can manage execution schedules. The ephemeral nature of containers make them a great environment for tasks that need to short execution times. The downside is that the configuration for this requires a bit more overhead and configuration beyond the host system and executable, which might not be ideal in scenarios where simplicity and speed of deployment are highly valued. + +#### Schedule controlled within container + +This approach is a hybrid of the two previous approaches, taking advantages and benefits from both. The advantage here is that the Docker image running in a container environment, can be built using a multi-stage docker file, which allows the entry point of the image to be defined at build time. This allows us to take advantage of the `crontab` process, which provides a configuration for 1) defining a process and 2) scheduling execution of that process. In this case, this process is the published console application, and can be configured to run on a schedule using a `crontab` file. + +### Recommendation + +After careful consideration, the approach of managing Cron within the Docker Image itself appears to be the best fit for our use case. This will provide customizable scheduling via a configurable file while allowing the execution to remain in the container environment, increasing security and testability. + +The steps for implementing the recommendation are provided below: + +#### Prepare the App Context + +The Docker file uses `WORKDIR /source` to reference the root of the application. That is to say `/source/Application/EdFi.AdminConsole.HealthCheck.Service` points to `/[your_local_repo_dir]/Application/EdFi.AdminConsole.HealthCheck.Service` + +The Docker file will also need to support exposing the configuration for the following environment variables: + +``` +{ + "AdminApiSettings": { + "ApiUrl": "https://localhost:7218/", + "AdminConsoleInstancesURI": "adminconsole/instances", + "AdminConsoleHealthCheckURI": "adminconsole/healthcheck", + "AccessTokenUrl": "https://localhost:7218/connect/token" + } +} +``` + +These can be exposed either by using the `AppSettingVariable__NestedKey=VALUE` accessor pattern to expose the values to the container. + +The `ClientId` and `ClientSecret` values will need to be retrieved from upon instance creation. + +#### Create a Cron file + +A cron file will need to be created at the root of your working dir. The file should contain the following: + +```c +* * * * * root dotnet /app/EdFi.AdminConsole.HealthCheckService.dll >> /var/log/achealthsvc.log + +``` + +This configuration is set to run every 1-min for testing. + +**Ensure that a new line is added to the end of the crontab file or the job will not run!** + +#### Update the Docker File + +The Docker File is updated to do the following: + +* Install `cron` +* Copy over the `crontab` file to `/etc/cron.d/container_cronjob +* Change permissions on the `container_cronjob` file +* Run `crontab` application + +An example of the Health Check Worker Dockerfile: + +```Dockerfile + +# Image based on .NET SDK to compile and publish the application +FROM mcr.microsoft.com/dotnet/sdk:8.0.401-alpine3.20@sha256:658c93223111638f9bb54746679e554b2cf0453d8fb7b9fed32c3c0726c210fe AS build + +WORKDIR /source + +# Copy source code and compile the application +COPY Application/EdFi.AdminConsole.HealthCheckService/. ./EdFi.AdminConsole.HealthCheckService/ + +WORKDIR /source/EdFi.AdminConsole.HealthCheckService + +# Restore dependencies, Then build and publish release +RUN dotnet restore &&\ +  dotnet publish -c Release -o /app + +# .NET Runtime image to execute the application +FROM mcr.microsoft.com/dotnet/runtime:8.0.11 AS runtime + +# Install Cron. +RUN apt-get update && apt-get install -y cron + +# Add cron from file and adjust permissions +COPY crontab /etc/cron.d/container_cronjob +RUN chmod 0644 /etc/cron.d/container_cronjob +RUN crontab /etc/cron.d/container_cronjob +WORKDIR /app + +# Add Published executable +COPY --from=build /app . + +# Execute the app via chron +CMD ["cron", "-f"] +``` + +### Add Docker compose for Development + +The `health-check-svs.yml` docker compose file provides a simple way to test building the image and logging the output. + +This might be moved or modified in the future to better support a multi-container environment. + +```yml +version: "3.9" + +services: + health-check-service: + build: + context: ../ + dockerfile: Docker/Dockerfile + volumes: + - service-logs:/var/log/achealthsvc.log + env_file: + - .env + +volumes: + service-logs: + name: health-check-service-logs + +``` + +## Testing Strategy + +The Docker file takes advantage of crontab file by piping the output of th cron executable to a log file. To ensure the executable is running, this log file is inspected to ensure the output from running the application is piped to the `achealthsvc.log` log file. + +To verify the schedule, a user can build the image using the Dockerfile and docker-compose included in the `/Docker/` directory. + +`docker compose -f .\Docker\health-check-svc.yml up` + +Can check that the job is in the table of jobs to run +`$ crontab -l` + +To verify the environment variables can be seen, in the container, once can use the `env` command from a connected shell, and inspect the values returned. + +## Additional Notes + +This holds any additional questions or comments that do not fit into the design spec above. + +### Outstanding Questions + +Will The Instance management worker need to be executed on a cron schedule? What would be executed? + +What is the difference between Admin API Client and Admin API Caller? + +How will the container image call the other services when hosted on localhost in other execution contexts? + +What is the expected deployment / execution environment for these services? + +* Docker compose okay when developing +* How will the workers be incorporated into the deployment + +Is Multi-tenancy something that needs to be accounted for? + +Can the crontab file read an environment variable? diff --git a/docs/design/adminconsole/admin-api-adminconsole-2.3-summary.md b/docs/design/adminconsole/admin-api-adminconsole-2.3-summary.md new file mode 100644 index 000000000..0debc9cbb --- /dev/null +++ b/docs/design/adminconsole/admin-api-adminconsole-2.3-summary.md @@ -0,0 +1,345 @@ + + +

Admin API Documentation adminconsole

+ +> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu. + +The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API. + +# Authentication + +- oAuth2 authentication. + + - Flow: clientCredentials + + - Token URL = [https://localhost/connect/token](https://localhost/connect/token) + +|Scope|Scope Description| +|---|---| +|edfi_admin_api/full_access|Unrestricted access to all Admin API endpoints| + +

Information

+ +## Retrieve API informational metadata + +`GET /` + +> Example responses + +> 200 Response + +```json +{ + "version": "string", + "build": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[informationResult](#schemainformationresult)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|[informationResult](#schemainformationresult)| + + + +

Adminconsole

+ +## get__adminconsole_odsinstances + +`GET /adminconsole/odsinstances` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|FeatureConstants.InternalServerErrorResponseDescription|None| + + + +## get__adminconsole_permissions + +`GET /adminconsole/permissions` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|FeatureConstants.InternalServerErrorResponseDescription|None| + + + +## get__adminconsole_steps + +`GET /adminconsole/steps` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|FeatureConstants.InternalServerErrorResponseDescription|None| + + + +## get__adminconsole_step + +`GET /adminconsole/step` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|query|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|FeatureConstants.InternalServerErrorResponseDescription|None| + + + +## get__adminconsole_tenants + +`GET /adminconsole/tenants` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|FeatureConstants.InternalServerErrorResponseDescription|None| + + + +## get__adminconsole_tenant + +`GET /adminconsole/tenant` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|query|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|FeatureConstants.InternalServerErrorResponseDescription|None| + + + +## get__adminconsole_userprofile + +`GET /adminconsole/userprofile` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|FeatureConstants.InternalServerErrorResponseDescription|None| + + + +

Connect

+ +## Registers new client + +`POST /connect/register` + +Registers new client + +> Body parameter + +```yaml +ClientId: string +ClientSecret: string +DisplayName: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» ClientId|body|string|false|Client id| +|» ClientSecret|body|string|false|Client secret| +|» DisplayName|body|string|false|Client display name| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Application registered successfully.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves bearer token + +`POST /connect/token` + +To authenticate Swagger requests, execute using "Authorize" above, not "Try It Out" here. + +> Body parameter + +```yaml +client_id: null +client_secret: null +grant_type: null +scope: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» client_id|body|string |false|none| +|» client_secret|body|string |false|none| +|» grant_type|body|string |false|none| +|» scope|body|string|false|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Sign-in successful.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +# Schemas + +

adminApiError

+ + + + + + +```json +{ + "title": "string", + "errors": [ + "string" + ] +} + +``` + +AdminApiError + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|title|string¦null|false|read-only|none| +|errors|[string]¦null|false|read-only|none| + +

informationResult

+ + + + + + +```json +{ + "version": "string", + "build": "string" +} + +``` + +Information + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|version|string|false|none|Application version| +|build|string|false|none|Build / release version| + +

registerClientRequest

+ + + + + + +```json +{ + "clientId": "string", + "clientSecret": "string", + "displayName": "string" +} + +``` + +RegisterClientRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|clientId|string|false|none|Client id| +|clientSecret|string|false|none|Client secret| +|displayName|string|false|none|Client display name| + diff --git a/docs/design/adminconsole/admin-api-adminconsole-2.3.yaml b/docs/design/adminconsole/admin-api-adminconsole-2.3.yaml new file mode 100644 index 000000000..dab723ae1 --- /dev/null +++ b/docs/design/adminconsole/admin-api-adminconsole-2.3.yaml @@ -0,0 +1,245 @@ +openapi: 3.0.1 +info: + title: Admin API Documentation + description: 'The Ed-Fi Admin API is a REST API-based administrative interface for managing vendors, applications, client credentials, and authorization rules for accessing an Ed-Fi API.' + version: adminconsole +paths: + /: + get: + tags: + - Information + summary: Retrieve API informational metadata + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/informationResult' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + content: + application/json: + schema: + $ref: '#/components/schemas/informationResult' + /adminconsole/odsinstances: + get: + tags: + - Adminconsole + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: FeatureConstants.InternalServerErrorResponseDescription + /adminconsole/permissions: + get: + tags: + - Adminconsole + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: FeatureConstants.InternalServerErrorResponseDescription + /adminconsole/steps: + get: + tags: + - Adminconsole + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: FeatureConstants.InternalServerErrorResponseDescription + /adminconsole/step: + get: + tags: + - Adminconsole + parameters: + - name: id + in: query + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: FeatureConstants.InternalServerErrorResponseDescription + /adminconsole/tenants: + get: + tags: + - Adminconsole + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: FeatureConstants.InternalServerErrorResponseDescription + /adminconsole/tenant: + get: + tags: + - Adminconsole + parameters: + - name: id + in: query + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: FeatureConstants.InternalServerErrorResponseDescription + /adminconsole/userprofile: + get: + tags: + - Adminconsole + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: FeatureConstants.InternalServerErrorResponseDescription + /connect/register: + post: + tags: + - Connect + summary: Registers new client + description: Registers new client + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + ClientId: + type: string + description: Client id + ClientSecret: + type: string + description: Client secret + DisplayName: + type: string + description: Client display name + encoding: + ClientId: + style: form + ClientSecret: + style: form + DisplayName: + style: form + responses: + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Application registered successfully. + /connect/token: + post: + tags: + - Connect + summary: Retrieves bearer token + description: "\nTo authenticate Swagger requests, execute using \"Authorize\" above, not \"Try It Out\" here." + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + client_id: + type: 'string' + client_secret: + type: 'string' + grant_type: + type: 'string' + scope: + type: string + responses: + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Sign-in successful. +components: + schemas: + adminApiError: + title: AdminApiError + type: object + properties: + title: + type: string + nullable: true + readOnly: true + errors: + type: array + items: + type: string + nullable: true + readOnly: true + additionalProperties: false + description: Wrapper schema for all error responses + informationResult: + title: Information + type: object + properties: + version: + type: string + description: Application version + build: + type: string + description: Build / release version + additionalProperties: false + registerClientRequest: + title: RegisterClientRequest + type: object + properties: + clientId: + type: string + description: Client id + clientSecret: + type: string + description: Client secret + displayName: + type: string + description: Client display name + additionalProperties: false + securitySchemes: + oauth: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://localhost/connect/token + scopes: + edfi_admin_api/full_access: Unrestricted access to all Admin API endpoints +security: + - oauth: + - api diff --git a/docs/design/adminconsole/developer.md b/docs/design/adminconsole/developer.md new file mode 100644 index 000000000..d11e5a1cd --- /dev/null +++ b/docs/design/adminconsole/developer.md @@ -0,0 +1,66 @@ + +# EF Core Migration and Database Update Commands + +This document provides step-by-step guidance for generating and applying migrations in EF Core. Commands for both Command Line (`cmd`) and Package Manager Console (`pmc`) are included. + +### 1. Add a Migration + +#### Command Line (`cmd`) + +Use the following command to add a migration in the command line: + +```bash +dotnet ef migrations add --context --output-dir Infrastructure/DataAccess/Artifacts/ --project EdFi.Ods.AdminApi.AdminConsole +``` + +#### Package Manager Console (`pmc`) + +In the Package Manager Console within Visual Studio, run: + +```powershell +Add-Migration -Context -Project EdFi.Ods.AdminApi.AdminConsole -OutputDir Infrastructure/DataAccess/Artifacts// +``` + +- `MigrationName`: Name of the migration (e.g., `InitialCreate`). +- `ContextName`: Name of the context (options: `AdminConsolePgSqlContext`,`AdminConsoleMsSqlContext`) +- `Database`: Name of the context (options: `Admin`,`Security`) +- `DbProvider`: The database provider, this will create a folder or add the migration in the specific db provider (options: `MsSql`,`PgSql` ). + +--- + +### 2. Update the Database + +#### Command Line (`cmd`) + +To update the database using the command line, run: + +```bash +dotnet ef database update --context --project EdFi.Ods.AdminApi.AdminConsole +``` + +#### Package Manager Console (`pmc`) + +For updating the database from the Package Manager Console, use: + +```powershell +Update-Database -Context -Project EdFi.Ods.AdminApi.AdminConsole +``` +- `ContextName`: Name of the context (options: `AdminConsolePgSqlContext`,`AdminConsoleMsSqlContext`) +--- + +### Summary + +| Action | Command (cmd) | Command (pmc) | +|---------------------|--------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| **Add Migration** | `dotnet ef migrations add --context --output-dir Infrastructure/DataAccess/Artifacts// --project EdFi.Ods.AdminApi.AdminConsole` | `Add-Migration -Context -Project EdFi.Ods.AdminApi.AdminConsole -OutputDir Infrastructure/DataAccess/Artifacts//` | +| **Update Database** | `dotnet ef database update --context --project EdFi.Ods.AdminApi.AdminConsole` | `Update-Database -Context -Project EdFi.Ods.AdminApi.AdminConsole` | + +--- + +>**Note:** Before running any commands, verify that the `appsettings.json` file has the right `DatabaseEngine` and the corresponding `connection string` correctly configured. + +> **Note:** Before running commands in cmd, make sure to navigate to the root folder of your project by using: +> +> ```bash +> cd path/to/your/project +> ``` diff --git a/docs/design/adminconsole/image-datahealth-ods.png b/docs/design/adminconsole/image-datahealth-ods.png new file mode 100644 index 000000000..9aa5c6d31 Binary files /dev/null and b/docs/design/adminconsole/image-datahealth-ods.png differ diff --git a/docs/design/adminconsole/image-db.png b/docs/design/adminconsole/image-db.png new file mode 100644 index 000000000..3e86de6c4 Binary files /dev/null and b/docs/design/adminconsole/image-db.png differ diff --git a/docs/design/adminconsole/notes.md b/docs/design/adminconsole/notes.md new file mode 100644 index 000000000..d98912503 --- /dev/null +++ b/docs/design/adminconsole/notes.md @@ -0,0 +1,103 @@ +# Admin Console + +We are going to expose the Admin Console endpoints required for the application in Admin API. It will be hosted as a different definition in Swagger and the base path
+```http://{domain}/adminconsole/{endpoint}``` + +## Endpoints + +[See documentation](./admin-api-adminconsole-2.3-summary.md)
+[See OpenAPI definition](./admin-api-adminconsole-2.3.yaml) + +The endpoints implementation will return a ExpandoObject type or a list of ExpandoObject in order to keep it dynamically when we need to adjust something in the payloads. + +Code reference using mock data: +> This example ilustrates using mock data but the final result will use a persistence using the EdFi_Admin database and other sources. See section [Persistence](#persistence) for more details. + +``` +internal Task GetSteps() +{ + using (StreamReader r = new StreamReader("Mockdata/data-steps.json")) + { + string json = r.ReadToEnd(); + List result = JsonConvert.DeserializeObject>(json); + return Task.FromResult(Results.Ok(result)); + } +} + +internal Task GetStep(int id) +{ + using (StreamReader r = new StreamReader("Mockdata/data-step.json")) + { + string json = r.ReadToEnd(); + ExpandoObject result = JsonConvert.DeserializeObject(json); + return Task.FromResult(Results.Ok(result)); + } +} +``` + +## Persistence + +Tables will be created under the new schema called 'adminconsole' in the EdFi_Admin database. +Tables will have the following design: + +* Doc ID - integer/PK +* OdsInstanceId - integer or UUID +* EdOrgID - integer (optional) +* UserID - integer (optional) +* Document - JSONB + +We identify the following tables to be created: + +* OdsInstances [See JSONB example](../../../Application/EdFi.Ods.AdminApi/Mockdata/data-odsinstances.json) +* Permissions [See JSONB example](../../../Application/EdFi.Ods.AdminApi/Mockdata/data-permissions.json) +* Steps [See JSONB example](../../../Application/EdFi.Ods.AdminApi/Mockdata/data-steps.json) +* Tenants [See JSONB example](../../../Application/EdFi.Ods.AdminApi/Mockdata/data-tenants.json) +* UserProfile [See JSONB example](../../../Application/EdFi.Ods.AdminApi/Mockdata/data-userprofile.json) + +> Some of these examples contain sensitive data such as keys and secrets values, so we are thinking of using an encryption/decryption mechanism to store the JSONB data by means of an AES256 alghoritm same as we use in Data Import application. + +![alt text](image-db.png) + +### Health check endpoint + +The healthcheck endpoint has informaction that comes from ODS/API + +* Healt check [See JSONB example](../../../Application/EdFi.Ods.AdminApi/Mockdata/data-healthcheck.json) + +In this case, we will store the client/secret values in Admin API to call some of the ODS/API endpoints to generate the returning payload to the Admin Console. The payload contains a 'total-count' report of some of the resources, it takes the value from the header in ODS/API. + +Example
+```https://api.ed-fi.org:443/v7.1/api/data/v3/ed-fi/studentSchoolAssociations?offset=0&limit=25&totalCount=true``` + +The parameter 'totalCount' is important to use because this will return us the count in the header as 'total-count'. With this value we can map it to our payload in the field called 'studentSchoolAssociations' + +![alt text](image-datahealth-ods.png) + +This process has to be called per field of the payload + +#### ODS/API services to call + +* studentSpecialEducationProgramAssociations +* studentDisciplineIncidentBehaviorAssociations +* studentSchoolAssociations +* studentSchoolAttendanceEvents +* studentSectionAssociations +* staffEducationOrganizationAssignmentAssociations +* staffSectionAssociations +* courseTranscripts +* sections + +> Unknown fields +> +> * basicReportingPeriodAttendances +> * reportingPeriodExts +> * localEducationAgencyId: We are assuming this as Ed Org Id but we are not sure about this +> * healthy (boolean): We are asumming this as a flag that return true if the above data have been populated correctly and no error from ODS/API + +As we have to call multiple endpoints in this one, we are considering use a caching approach (maybe the in-memory provided by .NET will be enough). If we want to refresh the data we can send a flag to the endpoint to do so. + +## Authentication and Authorization + +The endpoints will use the same Admin API authentication and authorization mechanism provided. Admin API uses [openiddict](https://github.com/openiddict/openiddict-core) library which supports different identity providers (besides its own) like Keycloak so we will have to integrate it in Admin API at some point. + +Reference: [OpenIdDict with Keycloak](https://damienbod.com/2022/05/02/implement-an-openiddict-identity-provider-using-asp-net-core-identity-with-keycloak-federation/) diff --git a/docs/design/adminconsole/readme.md b/docs/design/adminconsole/readme.md new file mode 100644 index 000000000..094fb3a20 --- /dev/null +++ b/docs/design/adminconsole/readme.md @@ -0,0 +1,137 @@ +# Admin API Design to Support the Admin App + +## Overview + +The Ed-Fi Admin App is a web-based user interface tool for managing Ed-Fi +ODS/API Platform installations. User can perform actions that include: + +* Manage tenants and database instances +* Manage Client credentials ("keys and secrets") +* Review application and database health + +The Ed-Fi Admin App application is a front-end only. The Ed-Fi Admin API 2 +application will act as the backend-for-frontend (BFF), serving all of the +interaction needs for Admin App. The Ed-Fi Admin API 2 will in turn rely on +other services and "worker" applications as needed to achieve some of its goals. + +The purpose of this document and related documents is to describe the +architecture of the related applications, the interfaces that sustain +communication, and the storage layers requirements. + +> [!TIP] +> The following sections utilize the [C4 model](https://c4model.com/) for +> describing the System Context and decomposing Containers within that Context. +> The notes further refine the architecture with detailed [UML sequence +> diagrams](https://en.wikipedia.org/wiki/Sequence_diagram). + +## System Context + +```mermaid +C4Context + title System Context for Ed-Fi Admin App + + Enterprise_Boundary(edorg, "Education Organization") { + Person(User, "Admin App User", "A system administrator") + } + + Enterprise_Boundary(other, "Other Services") { + System(Keycloak, "Keycloak", "OpenID Connect authorization provider") + } + + Enterprise_Boundary(edfi, "Ed-Fi ODS/API Platform") { + System(AdminConsole, "Ed-Fi Admin App", "A web application for managing ODS/API Deployments") + + + System_Boundary(backend, "Backend Systems") { + System(AdminAPI, "Ed-Fi Admin API 2 and Workers", "A REST API system for managing
administrative data and deployments,
plus background worker apps") + System(OdsApi, "Ed-Fi ODS/API", "A REST API system for
educational data interoperability") + } + } + + Rel(User, AdminConsole, "Manages instances,
Manages client credentials,
Checks API health") + UpdateRelStyle(User, AdminConsole, $offsetX="0", $offsetY="-10") + + Rel(AdminConsole, AdminAPI, "Issues HTTP requests") + UpdateRelStyle(AdminConsole, AdminAPI, $offsetY="-40", $offsetX="20") + + Rel(AdminAPI, OdsApi, "Reads and
configures") + UpdateRelStyle(AdminAPI, OdsApi, $offsetY="-20", $offsetX="-30") + + Rel(User, Keycloak, "Authentication") + UpdateRelStyle(User, Keycloak, $offsetX="-20", $offsetY="10") + + UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="2") +``` + +## Containers + +Two of the functions needed by Admin API 2 will benefit from background +execution, so that the Admin App end user can experience a quick response +time in the web browser. Two worker applications will provide the background +processing: + +1. Instance Management Worker - creates new database instances. +2. Health Check Worker - polls the ODS/API to find record counts and records + them in Admin API 2. + +```mermaid +C4Container + title "Admin API Containers" + + System(AdminConsole, "Ed-Fi Admin App", "A web application for managing ODS/API Deployments") + + System_Boundary(backend, "Backend Systems") { + + Boundary(b0, "Admin API") { + Container(AdminAPI, "Ed-Fi Admin API 2") + Container(HealthCheck, "Admin API Health
Check Worker") + Container(Instance, "Admin API Instance
Management Worker") + } + + Boundary(b1, "ODS/API") { + System(OdsApi, "Ed-Fi ODS/API", "A REST API system for
educational data interoperability") + SystemDb(ods3, "EdFi_ODS_") + } + + Boundary(b2, "Shared Databases") { + ContainerDb(Admin, "EdFi_Admin,
EdFi_Security") + } + } + + Rel(AdminConsole, AdminAPI, "Issues HTTP requests") + + Rel(HealthCheck, AdminAPI, "Reads ODS/API connections,
Writes health info") + UpdateRelStyle(HealthCheck, AdminAPI, $offsetY="50") + + Rel(HealthCheck, OdsApi, "Reads records counts") + UpdateRelStyle(HealthCheck, OdsApi, $offsetX="-60", $offsetY="20") + + Rel(Instance, AdminAPI, "Reads instance requests,
Write instance status") + UpdateRelStyle(Instance, AdminAPI, $offsetY="20", $offsetX="10") + + Rel(Instance, ods3, "Creates new ODS instances") + UpdateRelStyle(Instance, ods3, $offsetY="20", $offsetX="-50") + + Rel(OdsApi, ods3, "Reads and writes") + UpdateRelStyle(OdsApi, ods3, $offsetX="10") + + Rel(AdminAPI, Admin, "Reads and writes") + UpdateRelStyle(AdminAPI, Admin, $offsetY="50", $offsetX="10") + + Rel(OdsApi, Admin, "Reads") + UpdateRelStyle(OdsApi, Admin, $offsetY="20", $offsetX="-10") + + UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="2") +``` + +## Interfaces and Interactions + +The API interfaces and the interactions between specific containers are +described in detail in the following documents: + +* [REST API Support for Admin App](./APIS-FOR-ADMIN-CONSOLE.md) +* [Instance Management Worker](./INSTANCE-MANAGEMENT.md) +* [Health Check Worker](./HEALTH-CHECK-WORKER.md) + +Also see [Keycloak Configuration](./KEYCLOAK.md) for more information on using +Keycloak as the Open ID Connect provider. diff --git a/docs/design/auth/KEYCLOAK.md b/docs/design/auth/KEYCLOAK.md new file mode 100644 index 000000000..bb2b46fed --- /dev/null +++ b/docs/design/auth/KEYCLOAK.md @@ -0,0 +1,169 @@ +# Keycloak Configuration + +## How to Add Scopes in Keycloak + +This guide will walk you through the steps to add three scopes (edfi_admin_api/full_access, edfi_admin_api/tenant_access, and edfi_admin_api/worker) in Keycloak. + +### Prerequisites + +* Ensure you have Keycloak installed and running. +* Access to the Keycloak administration console. + +### Steps + +#### Step 1: Log in to the Keycloak Admin Console + +1. Open your web browser and navigate to the Keycloak admin console. +2. Enter your admin username and password, then click `Sign In`. + +#### Step 2: Select the Realm + +1. In the top-left corner of the console, click on the dropdown menu to select t + +#### Step 3: Navigate to Client Scopes + +1. In the left-hand menu, click on `Client Scopes` under the `Configure` section. +2. This will open the client scopes management page for the selected realm. + +#### Step 4: Create Scope edfi_admin_api/full_access + +1. Click the `Create` button to add a new client scope. +2. Fill in the following details: + * **Name**: Enter `edfi_admin_api/full_access` as the name for the scope. + * **Description**: (Optional) Provide a description for the scope. +3. Click the `Save` button to create the scope `edfi_admin_api/full_access`. + +#### Step 5: Create Scope edfi_admin_api/tenant_access + +1. Click the `Create` button again to add another client scope. +2. Fill in the following details: + * **Name**: Enter `edfi_admin_api/tenant_access` as the name for the scope. + * **Description**: (Optional) Provide a description for the scope. +3. Click the `Save` button to create the scope `edfi_admin_api/tenant_access`. + +#### Step 6: Create Scope edfi_admin_api/worker + +1. Click the `Create` button one more time to add a third client scope. +2. Fill in the following details: + * **Name**: Enter `edfi_admin_api/worker` as the name for the scope. + * **Description**: (Optional) Provide a description for the scope. +3. Click the `Save` button to create the scope `edfi_admin_api/worker`. + +## How to Add a Realm Role to a Realm + +### Prerequisites + +* Access to the Keycloak administration console. + +### Steps + +#### Step 1: Log in to the Keycloak Admin Console + +1. Open your web browser and navigate to the Keycloak admin console. +2. Enter your admin username and password, then click `Sign In`. + +#### Step 2: Select the Realm + +1. In the top-left corner of the console, click on the dropdown menu to select the desired realm. + +#### Step 3: Navigate to Roles + +1. In the left-hand menu, click on `Realm Roles` under the `Manage` section. +2. This will open the roles management page for the selected realm. + +#### Step 4: Add an adminapi-client + +1. Click the `Create Role` button at the top right corner of the roles management page. +2. Fill in the following details: + * **Role Name**: adminapi-client. + * **Description**: (Optional) Provide a description for the role. +3. Click the `Save` button to create the new role. + +#### Step 4: Add an adminconsole-user + +1. Click the `Create Role` button at the top right corner of the roles management page. +2. Fill in the following details: + * **Role Name**: adminconsole-user. + * **Description**: (Optional) Provide a description for the role. +3. Click the `Save` button to create the new role. + +#### Step 5: Configure adminconsole-user Role Settings + +1. Select the role adminconsole-user +2. Click the `Associated Roles` tab +3. Click the `Assign Role` button +4. Filter by realm roles +5. Check adminapi-client + +#### Step 6: Assign the Role to Users + +1. To assign the new realm role to users, navigate to the `Users` section in the left-hand menu. +2. Select the user you want to assign the role to. +3. Go to the `Role Mappings` tab. +4. In the `Available Roles` section, find and select the new realm role. +5. Click the `Add selected` button to assign the role to the user. + +## How to Add a Mapper to Realm Roles + +To add the claim `` to store the list of roles. + +### Prerequisites + +* Access to the Keycloak administration console. +* A realm with existing roles. + +### Steps + +#### Step 1: Log in to the Keycloak Admin Console + +1. Open your web browser and navigate to the Keycloak admin console. +2. Enter your admin username and password, then click `Sign In`. + +#### Step 2: Select the Realm + +1. In the top-left corner of the console, click on the dropdown menu to select the desired realm. +2. If you need to create a new realm, click `Add Realm` and follow the prompts. + +#### Step 3: Navigate to Client Scopes + +1. In the left-hand menu, click on `Client Scopes` under the `Configure` section. +2. This will open the client scopes management page for the selected realm. + +#### Step 4: Select or Create a Dedicated Scope + +1. Click on an existing client (admin-console) scope from the list. +2. If creating a new client scope, provide a name and description, then click `Save`. + +#### Step 5: Add a Mapper to the Dedicated Scope + +1. Within the selected client scope, navigate to the `Mappers` tab. +2. Click the `Add Mapper` button and select `From predefined mappers` to add a new mapper. +3. Select `realm roles` +4. Click on the `realm roles` link +5. Fill in the following details: + * **Name**: Enter a name for the new mapper. + * **Mapper Type**: Select `Role Name Mapper` from the dropdown. + * **Token Claim Name**: Enter the name "". + * **Claim JSON Type**: Select `String` or `Array` depending on your needs. + * **Add to ID token**: Check this box if you want to add the claim to the ID token. + * **Add to access token**: Check this box if you want to add the claim to the access token. + * **Add to userinfo**: Check this box if you want to add the claim to the userinfo response. +6. Click the `Save` button to create the new mapper. + +#### Step 6: Explain the Purpose of the Mapper + +The mapper created in the previous step is used to add a label "" to the tokens. By adding this label, you can ensure that the tokens contain the necessary information for your application's security and access control requirements. + +> [!IMPORTANT] +> Detailed notes to be filled out, replacing these bullet points. +> +> * Should make sure we can configure Keycloak once for use both by Admin API +> and by Admin Console. +> * Review the configuration script used by the Data Management Service. The +> configuration script approach is not required; Keycloak may have a better +> way of handling this. We just need to get both projects to use the same +> settings. Maybe create a Docker container for Ed-Fi Keycloak with shared +> settings already in place. +> * Need to describe how to create the role for the worker processes, which +> allows them to access client credentials via the `/adminconsole/instances` +> endpoint. diff --git a/docs/design/auth/README.md b/docs/design/auth/README.md new file mode 100644 index 000000000..35e4b6083 --- /dev/null +++ b/docs/design/auth/README.md @@ -0,0 +1,232 @@ +# Authentication and Authorization in Admin API 2.x + +Admin API 2 uses [OAuth 2](https://oauth.net/2/) and the [OpenID +Connect](https://openid.net/) ("OIDC") protocol for managing API authentication +and authorization. + +## Versions 2.0, 2.1, and 2.2 + +### System Context 2.0 through 2.2 + +System administrators interact directly with Admin API to perform ODS/API +configuration tasks and manage client credentials. Authentication and +authorization are [self-contained](./SELF-CONTAINED.md): there is no need for a +third party Identity Provider (IdP). There is a single `OAuth` scope available: +`edfi_admin_api/full_access`. + +```mermaid +C4Context + Person(SysAdmin, "Platform Host Sys Admin") + + Enterprise_Boundary(backend, "Ed-Fi ODS/API Platform") { + + System(AdminApi, "Ed-Fi Admin API 2", "A REST API system for
configuration of ODS/API
and management of
client credentials") + + System(OdsApi, "Ed-Fi ODS/API", "A REST API system for
educational data interoperability") + } + + Rel(SysAdmin, AdminApi, "Authenticates,
Interacts with `/v2` endpoints") + UpdateRelStyle(SysAdmin, AdminApi, $offsetX="0", $offsetY="-30") + + Rel(AdminApi, OdsApi, "Writes admin and
security configuration") + UpdateRelStyle(AdminApi, OdsApi, $offsetX="-20", $offsetY="40") +``` + +### Containers 2.0 through 2.2 + +```mermaid +C4Container + Person(SysAdmin, "Platform Host Sys Admin") + + System_Boundary(platform, "Ed-Fi ODS/API Platform") { + + Container(AdminApi, "Ed-Fi Admin API 2") + + Container(OdsApi, "Ed-Fi ODS/API") + + ContainerDb(Admin, "EdFi_Admin") + ContainerDb(Security, "EdFi_Security") + } + + Rel(SysAdmin, AdminApi, "Authenticates,
Interacts with `/v2` endpoints") + UpdateRelStyle(SysAdmin, AdminApi, $offsetX="0", $offsetY="-30") + + Rel(AdminApi, Admin, "Reads and writes") + UpdateRelStyle(AdminApi, Admin, $offsetY="0", $offsetX="10") + + Rel(OdsApi, Admin, "Reads") + UpdateRelStyle(OdsApi, Admin, $offsetY="20", $offsetX="-10") + + Rel(OdsApi, Security, "Reads") + UpdateRelStyle(OdsApi, Security, $offsetY="0", $offsetX="10") + + UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="2") +``` + +## Version 2.3 and above + +### Self-Contained and Third Party IdP + +The [self-contained authentication](./SELF-CONTAINED.md) in Admin API 2.0 +through 2.2 was adequate for the application's needs. This support provided +only the `client_credentials` grant flow. OpenIdDict _can_ support users, not +just clients, but the support was unnecessary at the time. With the introduction +of [Admin App](../adminconsole/readme.md), a user interface that is backed +by Admin API, there is a need for additional capabilities for managing users, +supporting other flows, and providing a user sign-in page. + +Therefore, when supporting Admin App, Admin API will need to rely on a token +generated by an Open ID Connect compatible IdP. The development team will use +Keycloak as their test implementation. However, by using Open ID Connect, it +should be possible to utilize other IdP's. + +For backwards compatibility, existing Admin API deployments will be able to +continue utilizing the self-contained authentication without the need to connect +a third party IdP. However, they will not be able to support Admin App in +this mode. + +### Differential Access Rights + +#### Client Authorization + +The Admin API now supports multiple applications, and the generated tokens will +need to reflect the client's role: + +* `adminapi-client`: all clients who access Admin API should have this role. +* `adminconsole-user`: user accounts that access the Admin App will _also_ have this role. + +Thus, the Admin API should reject any token that does not have the +`adminapi-client` role, and the Admin App should similarly reject a token +that does not have the `adminconsole-user` role. + +Roles will be described in the JWT with a new claim called +`http://schemas.microsoft.com/ws/2008/06/identity/claims/role`. This claim name +is for role-based access control in ASP.NET Identity. + +#### Resource Authorization + +Resources (endpoints) will be protected using OAuth Scopes. Admin API already +has a Scope of `edfi_admin_api/full_access` that provides access to all +reosurces. New scopes are needed for more limited access: + +* Admin App will have two user types: system administrators with full + access, and tenant administrators with more limited access. Details about this + more limited access are yet to be determined. **New scope: + `edfi_admin_api/tenant_access`**. +* The instance management and health check workers do not require full access to + the API. Their access can be limited with a **new scope: + `edfi_admin_api/worker`**. + +### System Context 2.3+ + +```mermaid +C4Context + Person(SysAdmin, "Platform Host Sys Admin") + + Enterprise_Boundary(backend, "Backend Systems") { + + Enterprise_Boundary(platform, "Ed-Fi ODS/API Platform") { + + System(AdminApi, "Ed-Fi Admin API 2", "A REST API system for
configuration of ODS/API
and management of
client credentials") + + System(OdsApi, "Ed-Fi ODS/API", "A REST API system for
educational data interoperability") + } + + Rel(SysAdmin, AdminApi, "HTTP Requests using IdP-generated token") + UpdateRelStyle(SysAdmin, AdminApi, $offsetX="0", $offsetY="100") + + Rel(AdminApi, OdsApi, "Writes admin and
security configuration") + UpdateRelStyle(AdminApi, OdsApi, $offsetX="-30", $offsetY="40") + + System_Ext(IdP, "IdP", "A compatible OpenID Connect Identity provider") + + Rel(SysAdmin, IdP, "OPTIONAL: Manage credentials,
OAuth2 token backend") + UpdateRelStyle(SysAdmin, IdP, $offsetX="10", $offsetY="-30") + } +``` + +### System Containers 2.3+ + +The containers can look exactly as in Admin API 2.0 through 2.2, or +alternatively can use the third party provider, which is shown below. Note that +Admin API does not need to have a direct connection to the IdP. + +```mermaid +C4Container + Person(SysAdmin, "Platform Host Sys Admin") + + Enterprise_Boundary(backend, "Backend Systems") { + + System_Boundary(platform, "Ed-Fi ODS/API Platform") { + Container(AdminApi, "Ed-Fi Admin API 2") + + Container(OdsApi, "Ed-Fi ODS/API") + + ContainerDb(Admin, "EdFi_Admin") + ContainerDb(Security, "EdFi_Security") + } + + System_Boundary(keycloakBoundary, "Identity Provider") { + Container_Ext(IdpWeb, "IdP Web/API") + ContainerDb_Ext(IdpDb, "IdP DB") + + Rel(IdpWeb, IdpDb, "Reads and
writes") + UpdateRelStyle(IdpWeb, IdpDb, $offsetX="-20", $offsetY="20") + } + + Rel(SysAdmin, AdminApi, "Authenticates,
Interacts with `/v2` endpoints") + UpdateRelStyle(SysAdmin, AdminApi, $offsetX="-10", $offsetY="-60") + + Rel(AdminApi, Admin, "Reads and writes") + UpdateRelStyle(AdminApi, Admin, $offsetY="0", $offsetX="10") + + Rel(OdsApi, Admin, "Reads") + UpdateRelStyle(OdsApi, Admin, $offsetY="20", $offsetX="-10") + + Rel(OdsApi, Security, "Reads") + UpdateRelStyle(OdsApi, Security, $offsetY="0", $offsetX="10") + + Rel(SysAdmin, IdpWeb, "Manage credentials,
OAuth2 token backend") + UpdateRelStyle(SysAdmin, IdpWeb, $offsetX="30", $offsetY="230") + } + + UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="1") +``` + +## Solution Design + +### Self-Contained Authentication with OpenIdDict + +Admin API integrates [OpenIdDict](https://openiddict.com/) directly into its own +application source code. Client credentials are created via the `/connect/register` endpoint following a +custom protocol. Tokens are generated via the `/connect/token` endpoint. + +This integration uses the following database tables: + +```mermaid +erDiagram + Applications ||..|{ Authorizations : has + Applications ||..|{ Tokens : "implicit key" + Authorizations }|..|| Scopes : "implicit key" +``` + +> [!NOTE] +> "implicit key" in this diagram means that there is no foreign key relationship +> in the database. The author does not know why there is no foreign key, but +> presumably it is for a good reason. + +### Third-Party OpenId Connect Provider + +Using appropriate .NET package dependencies, it is easy to setup the application +to use any OpenID Connect provider. At minimum, the following settings will be needed + +* Authority +* Audience +* Disable HTTPS certificate validation (that is, accept self-signed certificates) +* A signing key. Note: some libraries can retrieve this automatically with `GET + /.well-known/openid-configuration`. Might need to store that "well known" URL + in settings. See [COnfigure JWT bearer authentication in ASP.NET + Core](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-jwt-bearer-authentication?view=aspnetcore-8.0), + which refers to using the `MetadataAddress` to lookup a public signing key. + +_Also see: [Keycloak Integration](./KEYCLOAK.md)_ diff --git a/docs/design/docker-for-binaries/readme.md b/docs/design/docker-for-binaries/readme.md new file mode 100644 index 000000000..c2dc1f48d --- /dev/null +++ b/docs/design/docker-for-binaries/readme.md @@ -0,0 +1,116 @@ +# Docker for Binaries + +# User Story + +As an Ed-Fi system administrator, I want to run the Ed-Fi ODS Admin API in a container. + +# Acceptance Criteria + +* Built using the same NuGet packages that the PowerShell installer would use. +* Auto-published with the latest version available. +* Can use a pre-built Admin database image containing Admin API database schema. +* Can use an existing Admin database container, and deploy the database schema into it. +* Can use an existing Admin database instance that is not in a container, and deploy the database schema into it. + +# Implementation notes + +Today there are three binary-first Dockerfiles in Admin API: Docker/DB-Admin/pgsql/Dockerfile, mssql.Dockerfile, and pgsql.Dockerfile. + +The latter two contain the Admin API application runtime, with configuration differences for the two different database providers. + +Based on the notes above, this is the support matrix to test: + +| Admin API Host | Database Engine | Database Host | Admin DB Status | Dockerfiles +| ----------- | ----------- |----------- |----------- |----------- | +| Docker | PostgreSQL | Docker |New |pgsql/Dockerfile, pgsql.Dockerfile | +| Docker | PostgreSQL | External Host |New |pgsql.Dockerfile | +| Docker | PostgreSQL | Docker |Already exists |pgsql.Dockerfile | +| Docker | PostgreSQL | External Host |Already exists |pgsql.Dockerfile | +| Docker | MSSQL | External Host |New |mssql.Dockerfile | +| Docker | MSSQL | External Host |Already exists |mssql.Dockerfile | + +We do not have rows for MSSQL in Docker because, while it can be done, the Alliance does not provide those images due to licensing restrictions. + +The dockerfiles need to accept an argument with the version number, which can default to latest. +The CI build process can pass the correct argument to the docker build command (as seen in the Meadowlark on-prerelease.yml : docker-publish job). +There is a script that can be used for reference in order to get the available versions, sort and find latest. That script would then need to be run from inside the container, or it could be run outside the container before executing `docker build`. +`Get-NugetPackage` and `Invoke-SemnaticSort` are the appropriate commands for this scenario. +Link Reference: + +The Admin database image should no longer have a reference to the Admin App binary package. Instead, it should retrieve the NuGet package for AdminApi (there will only be one). +Use it to setup the database, then be sure to delete the NuGet package before finalizing the image. + +The dev team needs to brainstorm ways of handling the database install into existing containers or external database hosts. + +In order to structure the file layout for the packaged binaries, a distinction between "Nuget Packaged" and "Raw Source" versions should be created, for this purpose the follow approach could be applied: + +Create a docker directory at the root of the project, which contains these files: + +* db.pgsql.admin.Dockerfile + +* api.pgsql.Dockerfile or api.mssql.Dockerfile + +* The sample docker-compose.yml and other associated configuration files + +# Tasks + +* Build a Docker image for the Ed-Fi ODS Admin API +* Update the Admin database image to retrieve the NuGet package for Admin API. +* Run the Ed-Fi ODS Admin API in a Docker container with a PostgreSQL database. +* Run the Ed-Fi ODS Admin API in a Docker container with a Microsoft SQL Server database. +* Structure a file layout for the packaged binaries. + +# Research + +* Database install into existing container or external DB host. + +# Admin API Build Process + +```mermaid +flowchart LR + + H(Build Docker Image) + + subgraph Docker Build + A(Dockerfile) --> H + C(NuGet Packages) --> H + D(Version Argument) --> H + end + + subgraph CI Build Process + E(Build Process) --> H + F(Version Argument) --> H + end + + + H -- Publish --> G[[Admin API Docker Image]] +``` + +# Database Distribution and Deployment + +```mermaid +flowchart LR + subgraph Docker Image + C[[Admin API Docker Image]] + end + + subgraph PostgreSQL Database + D[(Admin Database Image/Container)] + E[(Existing Admin Database Container)] + F[(Existing Admin Database Instance)] + end + + subgraph Microsoft SQL Server Database External Host + G[(New Database)] + H[(Existing Database)] + end + + C --> D + C --> E + C --> F + + C --> G + C --> H + + +``` diff --git a/docs/developer.md b/docs/developer.md index 8c1fae95b..45ac00dfb 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,59 +2,59 @@ ## Contents -- [Admin API Developer Instructions](#admin-api-developer-instructions) - - [Contents](#contents) - - [Development Pre-Requisites](#development-pre-requisites) - - [Build Script](#build-script) - - [Running on Localhost](#running-on-localhost) - - [Configuring Admin API to Run with the ODS/API](#configuring-admin-api-to-run-with-the-odsapi) - - [Resetting the Database State](#resetting-the-database-state) - - [Running Locally in Docker](#running-locally-in-docker) - - [Testing Admin API](#testing-admin-api) - - [Application Architecture](#application-architecture) - - [Database Layer](#database-layer) - - [Validation](#validation) -- [Bulk Application Creation](#bulk-application-creation) +* [Admin API Developer Instructions](#admin-api-developer-instructions) + * [Contents](#contents) + * [Development Pre-Requisites](#development-pre-requisites) + * [Build Script](#build-script) + * [Running on Localhost](#running-on-localhost) + * [Configuring Admin API to Run with the ODS/API](#configuring-admin-api-to-run-with-the-odsapi) + * [Resetting the Database State](#resetting-the-database-state) + * [Running Locally in Docker](#running-locally-in-docker) + * [Using Keycloak (IDP)](#using-keycloak-idp) + * [Running Unit Tests, Integration Tests, and Generating Code Coverage Reports](#running-unit-tests-integration-tests-and-generating-code-coverage-reports) + * [Application Architecture](#application-architecture) + * [Database Layer](#database-layer) + * [Validation](#validation) ## Development Pre-Requisites -- [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet/6.0) -- Suggested to have either: - - [Visual Studio 2022](https://visualstudio.microsoft.com/downloads), or - - [Visual Studio 2022 Build +* [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +* Suggested to have either: + * [Visual Studio 2022](https://visualstudio.microsoft.com/downloads), or + * [Visual Studio 2022 Build Tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) (install the ".NET Build Tools" component) -- Clone [this +* Clone [this repository](https://github.com/Ed-Fi-Alliance-OSS/Ed-Fi-ODS-AdminApi) locally -- To work with the official Ed-Fi Docker solution, also clone the [Docker +* To work with the official Ed-Fi Docker solution, also clone the [Docker repository](https://github.com/Ed-Fi-Alliance-OSS/Ed-Fi-ODS-Docker). ## Build Script The PowerShell script `build.ps1` in the root directory contains functions for running standard build operations at the command line . This script assumes that -.NET 6.0 SDK or newer is installed. Other dependencies tools are downloaded +.NET 8.0 SDK or newer is installed. Other dependencies tools are downloaded as needed (nuget, nunit). Available command (e.g. `./build.ps1 clean`) (commands are not case sensitive): -- `clean`: runs `dotnet clean` -- `build`: runs `dotnet build` with several implicit steps +* `clean`: runs `dotnet clean` +* `build`: runs `dotnet build` with several implicit steps (clean, restore, inject version information). -- `unitTest`: executes NUnit tests in projects named `*.UnitTests`, which +* `unitTest`: executes NUnit tests in projects named `*.UnitTests`, which do not connect to a database. -- `integrationTest`: executes NUnit tests in projects named `*.Test`, +* `integrationTest`: executes NUnit tests in projects named `*.Test`, which connect to a database. Includes drop and deploy operations for installing fresh test databases compatible with Ed-Fi ODS/API 3.4 and 5.3. -- `buildAndTest`: executes the Build, UnitTest, and IntegrationTest +* `buildAndTest`: executes the Build, UnitTest, and IntegrationTest commands. -- `package`: builds pre-release and release NuGet packages for the Admin +* `package`: builds pre-release and release NuGet packages for the Admin App web application. -- `push`: uploads a NuGet package to the NuGet feed. -- `buildAndDeployToAdminApiDockerContainer`: runs the build operation, update +* `push`: uploads a NuGet package to the NuGet feed. +* `buildAndDeployToAdminApiDockerContainer`: runs the build operation, update the `appsettings.json` with provided Docker environment variables and copy over the latest files to existing AdminApi docker container for testing. -- `run`: runs the Admin API +* `run`: runs the Admin API Note: use the `IsLocalBuild` switch to install NuGet.exe if you do not already have it in your local path. @@ -66,10 +66,10 @@ arguments. There are three ways of running Admin API: -- `build.ps1 run` to run from the command line. -- Run inside a container with the help of +* `build.ps1 run` to run from the command line. +* Run inside a container with the help of [compose-build-dev.yml](../Docker/Compose/pgsql/compose-build-dev.yml). -- Start from Visual Studio to take advantage of easy debugging integration. +* Start from Visual Studio to take advantage of easy debugging integration. There are several launch profiles available either with `build.ps1` or when running from Visual Studio. Review @@ -90,9 +90,9 @@ Docker network from Admin API, because at present both need to access the same `EdFi_Admin` and `EdFi_Security` databases. If starting up manually, make sure that both Admin API and ODS/API have the same deployment setting: -- `sharedinstance` -- `yearspecific` -- `districtspecific` +* `sharedinstance` +* `yearspecific` +* `districtspecific` In Admin API, you set this value in the `appsettings.json` file under `AppSettings.ApiStartupType`. In the ODS/API, you set it as the @@ -158,20 +158,54 @@ eng/run-dbup-migrations.ps1 As mentioned above, you can run locally in Docker. See [docker.md](docker.md) for more information. -## Testing Admin API +### Using Keycloak (IDP) + +To use Keycloak for authenticating the API, you need to configure the parameters in the OIDC section. Additionally, you must specify with `"UseSelfcontainedAuthorization": false`, that the API’s own authentication will be disabled in favor of using Keycloak. + +Furthermore, when using Keycloak, the Register and Token endpoints will not be available in Swagger or for direct calls. Attempting to access these endpoints will result in a 404 error. + +```json +{ + "Authentication": { + "IssuerUrl": "", + "SigningKey": "", + "AllowRegistration": false, + "OIDC": { + "Authority": "https://localhost/auth/realms/edfi-admin-console", + "ValidateIssuer": true, + "RequireHttpsMetadata": false, + "EnableServerCertificateCustomValidationCallback": true + } + } +} +``` + +### Running Unit Tests, Integration Tests, and Generating Code Coverage Reports + +The source code includes two main types of test projects: -In source code there are two test projects: +* **Unit tests** (`*.UnitTests`): + * Run with: `build.ps1 -Command UnitTest` + * To collect code coverage, use: `build.ps1 -Command UnitTest -RunCoverageAnalysis` + This will generate an HTML report in the `coveragereport` directory. -- Unit tests that are completely isolated. -- The DB Tests project is a set of _integration_ tests that exercise the - repository layer. +* **Integration tests** (`*.DBTests`): + These tests exercise the repository layer and require a database connection. + * Run with: `build.ps1 -Command IntegrationTest` + * To collect code coverage, use: `build.ps1 -Command IntegrationTest -RunCoverageAnalysis` + This will also generate an HTML report in the `coveragereport` directory. -Additionally there is a set of end-to-end (E2E) tests in Postman. See the [E2E -Tests/README.md](../Application/EdFi.Ods.AdminApi/E2E%20Tests/README.md) for -more information on these tests. +Alternatively, you can run both unit and integration tests together with: +`build.ps1 -Command BuildAndTest [-RunCoverageAnalysis]` -All three of these test suites should be 100% green before merging new code into -the `main` branch. +> [!NOTE] +> Code coverage analysis requires the `reportgenerator` tool. +> Install it with: `dotnet tool install -g dotnet-reportgenerator-globaltool` + +Additionally, there is a set of end-to-end (E2E) tests in Postman. +See [E2E Tests/README.md](../Application/EdFi.Ods.AdminApi/E2E%20Tests/README.md) for more information. + +All three test suites should pass successfully before merging new code into the `main` branch. ## Application Architecture @@ -200,52 +234,3 @@ credentials. Validation of API requests is configured via [FluentValidation](https://docs.fluentvalidation.net/en/latest/). - -### Bulk Application Creation - -This PowerShell script creates multiple vendors and applications upon execution and stores the generated keys/secrets in a file. -The script utilizes a CSV file as input to specify the vendors and applications to be created. -The script prevents duplicate creation of vendors and applications by skipping existing combinations. -Remember to store keys/secrets securely. - -## Instructions -1. Download the code: `git@github.com:Ed-Fi-Alliance-OSS/AdminAPI-1.x.git` -2. Open a PowerShell window in Administrator mode and navigate to the /eng/bulk-key-creation folder. -3. Populate the sample CSV with the required vendor and application information. -4. Run the following PowerShell command to load modules for Applications creation: - ``` - Import-Module .\Bulk-EdFiOdsApplications.psm1 - ``` -5. The Bulk Register Applications can take a number of parameters, copy and modify the following parameter code to fit your needs: - - ``` - $parameters = @{ - CSVFilePath = "District details.csv" - BaseApiUrl = "https://localhost/AdminApi" - NamespacePrefixes = "uri://ed-fi.org/" - Key = "YourAdminApiUser" - Secret = "yourAdminApiPassword" - ClaimsetName = "District Hosted SIS Vendor" - logRootPath = "Logs" - } - ``` - -6. Run the following command in the PowerShell window: - ``` - Bulk-EdFiOdsApplications $parameters - ``` - -7. A new file will be create with the Key and Secrets -###### Parameters definition - * ` CSVFilePath `The csv file to be processed - * ` BaseApiUrl `The Admin Api url, Example: https://localhost/AdminApi1x - * ` namespacePrefixes `The namespace for the vendor, Example: uri://ed-fi.org/ - * ` Key `The Key to authenticate with the API - * ` Secret `The Secret to authenticate with the API - * ` ClaimsetName `The claim name that will be assigned to the application, Ex: "District Hosted SIS Vendor" - * ` logRootPath `The Path where you could find the log file - - -### Logs - -By default, the script creates log files, to review them go to the root directory and find the Logs folder. diff --git a/docs/docker.md b/docs/docker.md index c3392e6fa..9d3cc5c39 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -4,6 +4,8 @@ Must already have Docker Desktop or equivalent running on your workstation. ## Quick Start for Local Development and Testing +PostgreSQL + ```mermaid graph LR A(Admin Client) -->|HTTPS| B[nginx] @@ -61,39 +63,53 @@ style I fill:#fff bash ./generate-certificate.sh ``` -2. Copy and customize the `.env.example` file. Importantly, be sure to change - the encryption key. In a Bash prompt, generate a random key thusly: `openssl +2. Copy and customize the `.env.example` file. The project has a PostgreSQL + version (Docker/Compose/pgsql) and a MSSQL version (Docker/Compose/mssql) + to run the containers. Importantly, be sure to change the encryption key. + In a Bash prompt, generate a random key thusly: `openssl rand -base64 32`. + PostgreSQL + + ```shell + cd Docker/Compose/pgsql + cp .env.example .env + code .env + ``` + + MSSQL + ```shell - cd ../../Compose/pgsql + cd Docker/Compose/mssql cp .env.example .env code .env ``` + > [!NOTE] + > The .env file is a shared resource that can be referenced by both the + > "MultiTenant" and "SingleTenant" compose files. + 3. Build local containers (optional step; next step will run the build implicitly) ```shell - docker compose -f compose-build-dev.yml build + docker compose -f SingleTenant/compose-build-dev.yml build ``` 4. Start containers ```shell - docker compose -f compose-build-dev.yml up -d + docker compose -f SingleTenant/compose-build-dev.yml up -d ``` 5. Inspect containers ```shell # List processes - docker compose -f compose-build-dev.yml ps + docker compose -f SingleTenant/compose-build-dev.yml ps # Check status of the AdminAPI curl -k https://localhost/adminapi - # Check status of ODS/API - curl -k https://localhost/webapi ``` 6. Create an administrative (full access) API client (substitute in appropriate @@ -155,4 +171,20 @@ style D fill:#fff ``` Instructions are similar to the localhost quickstart above, except use -`compose-build-binaries.yml` instead of `compose-build-dev.yml`. +`compose-build-binaries.yml`, `compose-build-idp-binaries.yml` or `compose-build-idp-dev.yml` instead of `compose-build-dev.yml`. + +## Multi-Tenant + +Instructions are similar to the Local Development and Pre-Built Binaries setups above. + +Tenants details can be configured on appsettings.dockertemplate.json file. + +For local development and testing, use `MultiTenant/compose-build-dev-multi-tenant.yml`. +For local development and testing with keycloak, use `MultiTenant/compose-build-idp-dev-multi-tenant.yml`. +For testing pre-built binaries, use `MultiTenant/compose-build-binaries-multi-tenant.yml`. +For testing pre-built binaries with keycloak, use `MultiTenant/compose-build-idp-binaries-multi-tenant.yml`. + +## Admin Api and Ed-Fi ODS / API docker containers + +Please refer [DOCKER DEPLOYMENT](https://techdocs.ed-fi.org/display/EDFITOOLS/Docker+Deployment) for +installing and configuring Admin Api along with Ed-Fi ODS / API on Docker containers for testing. diff --git a/docs/http/apiClients.http b/docs/http/apiClients.http new file mode 100644 index 000000000..ffacea0f5 --- /dev/null +++ b/docs/http/apiClients.http @@ -0,0 +1,98 @@ +# This file is intended for use with Admin API 2 running in multi-tenant mode, +# along with ODS/API 7.x in multi-tenant mode. It assumes there are two +# different tenants named "tenant1" and "tenant2". Each has only one ODS +# instance. + +@adminapi_url=https://localhost:7214 +@adminapi_client=adminapi_client +@adminapi_secret=adminapi_SECRET_2025_rftyguhijkotgyhuijok + +### Register a new client +POST {{adminapi_url}}/connect/register +Content-Type: application/x-www-form-urlencoded + +ClientId={{adminapi_client}}&ClientSecret={{adminapi_secret}}&DisplayName=Admin+API+{{adminapi_client}} + +### Get a token +# @name tokenRequest +POST {{adminapi_url}}/connect/token +Content-Type: application/x-www-form-urlencoded +Authorization: basic {{adminapi_client}}:{{adminapi_secret}} + +grant_type=client_credentials&scope=edfi_admin_api/full_access + +### +@token={{tokenRequest.response.body.access_token}} + + +### Get ods instances +GET {{adminapi_url}}/v2/odsinstances +Content-Type: application/json +Authorization: bearer {{token}} + + +### Get application +GET {{adminapi_url}}/v2/applications/1 +Content-Type: application/json +Authorization: bearer {{token}} + + +### Get all apiclients by application +GET {{adminapi_url}}/v2/apiclients?applicationid=1 +Content-Type: application/json +Authorization: bearer {{token}} + + +### Create ApiClient +POST {{adminapi_url}}/v2/apiclients +Content-Type: application/json +Authorization: bearer {{token}} + +{ + "name": "ApiClient1", + "isApproved": true, + "applicationId": 1, + "odsInstanceIds": [ + 1 + ] +} + +### Update ApiClient +PUT {{adminapi_url}}/v2/apiclients/2 +Content-Type: application/json +Authorization: bearer {{token}} + +{ + "name": "ApiClient1 updated", + "isApproved": true, + "applicationId": 1, + "odsInstanceIds": [ + 1 + ] +} + +### get apiclient +GET {{adminapi_url}}/v2/apiclients/2 +Content-Type: application/json +Authorization: bearer {{token}} + + +### Reset apiclient secret +PUT {{adminapi_url}}/v2/apiclients/2/reset-credential +Content-Type: application/json +Authorization: bearer {{token}} + + + + + +### Reset application secret +PUT {{adminapi_url}}/v2/applications/2/reset-credential +Content-Type: application/json +Authorization: bearer {{token}} + + +### delete apiclient +DELETE {{adminapi_url}}/v2/apiclients/2 +Content-Type: application/json +Authorization: bearer {{token}} diff --git a/docs/http/claimsets.http b/docs/http/claimsets.http new file mode 100644 index 000000000..d0ba9275d --- /dev/null +++ b/docs/http/claimsets.http @@ -0,0 +1,37 @@ +# This file is intended for use with Admin API 2 running in multi-tenant mode, +# along with ODS/API 7.x in multi-tenant mode. It assumes there are two +# different tenants named "tenant1" and "tenant2". Each has only one ODS +# instance. + +@adminapi_url=https://localhost:7214 +@adminapi_client=adminapi_client2 +@adminapi_secret=adminapi_SECRET_2025_rftyguhijkotgyhuijok + +### Register a new client +POST {{adminapi_url}}/connect/register +Content-Type: application/x-www-form-urlencoded + +ClientId={{adminapi_client}}&ClientSecret={{adminapi_secret}}&DisplayName=Admin+API+{{adminapi_client}} + +### Get a token +# @name tokenRequest +POST {{adminapi_url}}/connect/token +Content-Type: application/x-www-form-urlencoded +Authorization: basic {{adminapi_client}}:{{adminapi_secret}} + +grant_type=client_credentials&scope=edfi_admin_api/full_access + +### +@token={{tokenRequest.response.body.access_token}} + + +### Get claimsets V1 +GET {{adminapi_url}}/v1/claimsets +Content-Type: application/json +Authorization: bearer {{token}} + + +### Get claimsets V2 +GET {{adminapi_url}}/v2/claimsets +Content-Type: application/json +Authorization: bearer {{token}} diff --git a/docs/http/profiles.http b/docs/http/profiles.http new file mode 100644 index 000000000..b18967ab6 --- /dev/null +++ b/docs/http/profiles.http @@ -0,0 +1,75 @@ +# This file is intended for use with Admin API 2 running in multi-tenant mode, +# along with ODS/API 7.x in multi-tenant mode. It assumes there are two +# different tenants named "tenant1" and "tenant2". Each has only one ODS +# instance. + +@adminapi_url=https://localhost:7214 +# @adminapi_url=https://localhost/adminapi + +@adminapi_client=adminapi_client2 +@adminapi_secret=adminapi_SECRET_2025_rftyguhijkotgyhuijok + +### Register a new client +POST {{adminapi_url}}/connect/register +Content-Type: application/x-www-form-urlencoded +Tenant: tenant2 + +ClientId={{adminapi_client}}&ClientSecret={{adminapi_secret}}&DisplayName=Admin+API+{{adminapi_client}} + +### Get a token +# @name tokenRequest +POST {{adminapi_url}}/connect/token +Content-Type: application/x-www-form-urlencoded +Authorization: basic {{adminapi_client}}:{{adminapi_secret}} +Tenant: tenant2 + +grant_type=client_credentials&scope=edfi_admin_api/full_access + +### +@token={{tokenRequest.response.body.access_token}} + + +### Get profiles +GET {{adminapi_url}}/v2/profiles +Content-Type: application/json +Authorization: bearer {{token}} + +### Create profile 1 +POST {{adminapi_url}}/v2/profiles +Content-Type: application/json +Authorization: bearer {{token}} + +{ + "name": "profile1", + "definition": " + + + + + + + + + + + +" +} + +# Given the line breaks it should return 400 with a descriptive error message: +# { +# "message": "The request body contains malformed JSON. Please ensure your data is properly formatted and try again." +# } + + +### Create profile 2 +POST {{adminapi_url}}/v2/profiles +Content-Type: application/json +Authorization: bearer {{token}} + +{ + "name": "profile1", + "definition": " " +} + + diff --git a/docs/http/tenants.http b/docs/http/tenants.http new file mode 100644 index 000000000..333215cce --- /dev/null +++ b/docs/http/tenants.http @@ -0,0 +1,62 @@ +# This file is intended for use with Admin API 2 running in multi-tenant mode, +# along with ODS/API 7.x in multi-tenant mode. It assumes there are two +# different tenants named "tenant1" and "tenant2". Each has only one ODS +# instance. + +# @adminapi_url=https://localhost/adminapi +@adminapi_url=https://localhost/adminapi + +@adminapi_client=adminapi_client22 +@adminapi_secret=adminapi_SECRET_2025_rftyguhijkotgyhuijok + +### Register a new client +POST {{adminapi_url}}/connect/register +Content-Type: application/x-www-form-urlencoded +Tenant: tenant1 + +ClientId={{adminapi_client}}&ClientSecret={{adminapi_secret}}&DisplayName=Admin+API+{{adminapi_client}} + +### Get a token +# @name tokenRequest +POST {{adminapi_url}}/connect/token +Content-Type: application/x-www-form-urlencoded +Authorization: basic {{adminapi_client}}:{{adminapi_secret}} +Tenant: tenant1 + +grant_type=client_credentials&scope=edfi_admin_api/full_access + +### +@token={{tokenRequest.response.body.access_token}} + +### Get tenants V2 +GET {{adminapi_url}}/v2/tenants +Content-Type: application/json +Authorization: bearer {{token}} + +### Get tenant V2 by id +GET {{adminapi_url}}/v2/tenants/default +Content-Type: application/json +Authorization: bearer {{token}} +Tenant: tenant1 + +### Create tenant +POST {{adminapi_url}}/v2/tenants +Content-Type: application/json +Authorization: bearer {{token}} +Tenant: tenant1 + +{ + "TenantName": "tenant3", + "EdFiSecurityConnectionString": "123", + "EdFiAdminConnectionString": "Data Source=db-admin;Initial Catalog=EdFi_Admin;User Id=edfi;Password=P@55w0rd;Encrypt=false;TrustServerCertificate=true" +} + +### Create tenant no connection strings +POST {{adminapi_url}}/v2/tenants +Content-Type: application/json +Authorization: bearer {{token}} +Tenant: tenant1 + +{ + "TenantName": "tenantNoConnStrings2" +} \ No newline at end of file diff --git a/docs/http/token-demonstration.http b/docs/http/token-demonstration.http new file mode 100644 index 000000000..1fde09d9b --- /dev/null +++ b/docs/http/token-demonstration.http @@ -0,0 +1,42 @@ +# This file is intended for use with Admin API 2 running in multi-tenant mode, +# along with ODS/API 7.x in multi-tenant mode. It assumes there are two +# different tenants named "tenant1" and "tenant2". Each has only one ODS +# instance. + +@adminapi_url=https://localhost:7214 +@adminapi_client=adminapi_client +@adminapi_secret=adminapi_SECRET_2025_rftyguhijkotgyhuijok + +### Register a new client +POST {{adminapi_url}}/connect/register +Content-Type: application/x-www-form-urlencoded + +ClientId={{adminapi_client}}&ClientSecret={{adminapi_secret}}&DisplayName=Admin+API+{{adminapi_client}} + +### Get a token +# @name tokenRequest +POST {{adminapi_url}}/connect/token +Content-Type: application/x-www-form-urlencoded +Authorization: basic {{adminapi_client}}:{{adminapi_secret}} + +grant_type=client_credentials&scope=edfi_admin_api/full_access + +### +@token={{tokenRequest.response.body.access_token}} + +### Use the token +GET {{adminapi_url}}/v2/vendors +Authorization: bearer {{token}} + +### Use an expired token +GET {{adminapi_url}}/v2/vendors +Authorization: bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IlBLUVBDSFFYUko5UDdMU0RCSllTUlZSRkREVFFYVkFTSV9COVdXTUsiLCJ0eXAiOiJhdCtqd3QifQ.eyJzdWIiOiJhZG1pbmFwaV9jbGllbnQiLCJvaV9wcnN0IjoiYWRtaW5hcGlfY2xpZW50IiwiY2xpZW50X2lkIjoiYWRtaW5hcGlfY2xpZW50Iiwib2lfdGtuX2lkIjoiMiIsInNjb3BlIjoiZWRmaV9hZG1pbl9hcGkvZnVsbF9hY2Nlc3MiLCJqdGkiOiJhYmY4NjQ1MC05OTIwLTQyODItYmYyMy1jZmU2ZGI1MTQwN2YiLCJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MjE0LyIsImV4cCI6MTczODc5NzcwNCwiaWF0IjoxNzM4Nzk1OTA0fQ.BIkw1JjA6KqJHquZd8r_h6MOi-89cn6QvQfwr1Z1Zohnn9cCRkV3c_bDupF7abxhI1dNoKvD1zYeFzUvfdfG4rlK9aXD_XzkbvF5gZkRV6hxaZlSaT0Vp5l02ZENjsIryPTBDYFXtJIEEf0OJ9N2EIaeEf0pwBrpyeLagJAC13XiCKQWO8oM0i1YgsardPuWfhujQdn4RErIcvyWRGQNOGkpzUgU_29LpvM_I_cPLyvQLi2KOlxJAiJceTgxa9ARYoVpiUBIb9QS6XpAWTy4bhbtFuz9KAm0BzawucXvSkUKA4CmMdMkkUlXHhvFtsiYSicnFkUB6nhOZudrRqdQCg + +### Use a corrupt token (bad signature) +GET {{adminapi_url}}/v2/vendors +Authorization: bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IlBLUVBDSFFYUko5UDdMU0RCSllTUlZSRkREVFFYVkFTSV9COVdXTUsiLCJ0eXAiOiJhdCtqd3QifQ.eyJzdWIiOiJhZG1pbmFwaV9jbGllbnQiLCJvaV9wcnN0IjoiYWRtaW5hcGlfY2xpZW50IiwiY2xpZW50X2lkIjoiYWRtaW5hcGlfY2xpZW50Iiwib2lfdGtuX2lkIjoiMiIsInNjb3BlIjoiZWRmaV9hZG1pbl9hcGkvZnVsbF9hY2Nlc3MiLCJqdGkiOiJhYmY4NjQ1MC05OTIwLTQyODItYmYyMy1jZmU2ZGI1MTQwN2YiLCJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MjE0LyIsImV4cCI6MTczODc5NzcwNCwiaWF0IjoxNzM4Nzk1OTA0fQ.BIkw1JjA6KqJHquZd8r_h6MOi-89cn6QvQfwr1Z1Zohnn9cCRkV3c_bDupF7abxhI1dNoKvD1zYeFzUvfdfG4rlK9aXD_XzkbvF5gZkRV6hxaZlSaT0Vp5l02ZENjsIryPTBDYFXtJIEEf0OJ9N2EIaeEf0pwBrpyeLagJAC13XiCKQWO8oM0i1YgsardPuWfhujQdn4RErIcvyWRGQNOGkpzUgU_29LpvM_I_cPLyvQLi2KOlxJAiJceTgxa9ARYoVpiUBIb9QS6XpAWTy4bhbtFuz9KAm0BzawucXvSkUKA4CmMdMkkUlXHhvFtsiYSicnFkUB6nhOZudrRqdQcG + +### Use a corrupt token (bad header) +GET {{adminapi_url}}/v2/vendors +Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbmFwaV9jbGllbnQiLCJvaV9wcnN0IjoiYWRtaW5hcGlfY2xpZW50IiwiY2xpZW50X2lkIjoiYWRtaW5hcGlfY2xpZW50Iiwib2lfdGtuX2lkIjoiMiIsInNjb3BlIjoiZWRmaV9hZG1pbl9hcGkvZnVsbF9hY2Nlc3MiLCJqdGkiOiJhYmY4NjQ1MC05OTIwLTQyODItYmYyMy1jZmU2ZGI1MTQwN2YiLCJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MjE0LyIsImV4cCI6MTczODc5NzcwNCwiaWF0IjoxNzM4Nzk1OTA0fQ.BIkw1JjA6KqJHquZd8r_h6MOi-89cn6QvQfwr1Z1Zohnn9cCRkV3c_bDupF7abxhI1dNoKvD1zYeFzUvfdfG4rlK9aXD_XzkbvF5gZkRV6hxaZlSaT0Vp5l02ZENjsIryPTBDYFXtJIEEf0OJ9N2EIaeEf0pwBrpyeLagJAC13XiCKQWO8oM0i1YgsardPuWfhujQdn4RErIcvyWRGQNOGkpzUgU_29LpvM_I_cPLyvQLi2KOlxJAiJceTgxa9ARYoVpiUBIb9QS6XpAWTy4bhbtFuz9KAm0BzawucXvSkUKA4CmMdMkkUlXHhvFtsiYSicnFkUB6nhOZudrRqdQCg + diff --git a/docs/http/vendors.http b/docs/http/vendors.http new file mode 100644 index 000000000..3c7557900 --- /dev/null +++ b/docs/http/vendors.http @@ -0,0 +1,70 @@ +# This file is intended for use with Admin API 2 running in multi-tenant mode, +# along with ODS/API 7.x in multi-tenant mode. It assumes there are two +# different tenants named "tenant1" and "tenant2". Each has only one ODS +# instance. + +@adminapi_url=https://localhost:7214 +@adminapi_client=adminapi_client2 +@adminapi_secret=adminapi_SECRET_2025_rftyguhijkotgyhuijok + +### Register a new client +POST {{adminapi_url}}/connect/register +Content-Type: application/x-www-form-urlencoded + +ClientId={{adminapi_client}}&ClientSecret={{adminapi_secret}}&DisplayName=Admin+API+{{adminapi_client}} + +### Get a token +# @name tokenRequest +POST {{adminapi_url}}/connect/token +Content-Type: application/x-www-form-urlencoded +Authorization: basic {{adminapi_client}}:{{adminapi_secret}} + +grant_type=client_credentials&scope=edfi_admin_api/full_access + +### +@token={{tokenRequest.response.body.access_token}} + + +### Create vendor without namespace +POST {{adminapi_url}}/v2/vendors +Content-Type: application/json +Authorization: bearer {{token}} + +{ + "company": "vendor3 - no namespace", + "contactName": "David Jiménez", + "contactEmailAddress": "david.jimenez@example.com" +} + +### Create vendor with multiple namespaces +POST {{adminapi_url}}/v2/vendors +Content-Type: application/json +Authorization: bearer {{token}} + +{ + "company": "vendor3 - multiple namespaces", + "namespacePrefixes": "uri://ed-fi.org/2000, uri://ed-fi.org/2015, uri://ed-fi.org/2023", + "contactName": "David Jiménez", + "contactEmailAddress": "david.jimenez@example.com" +} + + +### Get vendors V1 +GET {{adminapi_url}}/v2/vendors +Content-Type: application/json +Authorization: bearer {{token}} + +### Get vendor V2 by id +GET {{adminapi_url}}/v2/vendors/2 +Content-Type: application/json +Authorization: bearer {{token}} + +### Get vendors V2 +GET {{adminapi_url}}/v2/vendors +Content-Type: application/json +Authorization: bearer {{token}} + +### Get vendors V2 with namespace filter +GET {{adminapi_url}}/v2/vendors?namespacePrefixes=uri://ed-fi.org/2015 +Content-Type: application/json +Authorization: bearer {{token}} diff --git a/docs/migration-guide-docker.md b/docs/migration-guide-docker.md new file mode 100644 index 000000000..16b2989dd --- /dev/null +++ b/docs/migration-guide-docker.md @@ -0,0 +1,145 @@ +# Migrate from previous versions (Docker container) + +You can find the preparation steps to publish the binaries in the following [Migration Guide](./migration-guide.md) + +## Update AdminApi Docker Container + +### Step 1: Pack the binaries + +Go to the publish directory + +```bash +cd Application\EdFi.Ods.AdminApi\publish +``` + +Now, pack the binaries into a tar package. + +```bash +tar --exclude='appsettings*.json' --exclude='*.log' --exclude='*.sh' -cvf adminApi_publish.tar *.* +``` + +## Step 2: Identify the Docker container + +To update the Docker container you need to run the following command to get the Docker Container Id. + +```bash +docker ps --format '{{.ID}} {{.Image}} {{.Names}}' +``` + +The result of this command will be like the following + +| CONTAINER ID | IMAGE | NAMES | +| -- | -- | -- | +| 91d478e194d7 | singletenant-adminapi | adminapi-packaged +| 35afe7e06bdc | singletenant-nginx | ed-fi-gateway-adminapi-packaged | +| 81c223f544f7 | singletenant-db-admin | ed-fi-db-admin-adminapi + +You will need the Container Id for the adminapi container to run the following commands. + +## Step 3: Copy package to docker container + +Using the container id, replace the with the corresponding Container Id for the adminapi + +```bash +docker cp adminApi_publish.tar :/tmp/adminApi_publish.tar +``` + +## Step 4: Remove dll files from the container + +To update the application you need to remove the previous dll files. The new version has new versions of the dll files and also some packages were removed to fix vulnerabilities. + +```bash +docker exec -it sh -c "find /app -type f ! -name '*.sh' ! -name '*.config' ! -name 'appsettings*.json' -exec rm -rf {} +" +``` + +## Step 5: Unzip the tar file into the Docker container + +Now, you will need to unzip the binaries into the Docker container folder. + +```bash +docker exec -it sh -c "tar -xvf /tmp/adminApi_publish.tar -C /app/" +``` + +## Step 6: Update the appsettings file + +The appsettings should be updated to add some parameters. + +### 6.1 Download appsettings.json + + First, download the appsettings.json from the Docker container to edit the file on the local computer + +```bash +# For Windows +docker cp :/app/appsettings.json /temp/appsettings.json +``` + +```bash +# For Linux +docker cp :/app/appsettings.json /tmp/appsettings.json +``` + +### 6.2 Edit appsettings.json file on the local computer + +Using a text editor add the following lines. + +For the AppSettings section add the parameter + +``` +"PreventDuplicateApplications": "false" +``` + +After the AllowedHosts parameter, add the following section + +``` +"IpRateLimiting": { + "EnableEndpointRateLimiting": true, + "StackBlockedRequests": false, + "RealIpHeader": "X-Real-IP", + "ClientIdHeader": "X-ClientId", + "HttpStatusCode": 429, + "IpWhitelist": [], + "EndpointWhitelist": [], + "GeneralRules": [ + { + "Endpoint": "POST:/Connect/Register", + "Period": "1m", + "Limit": 3 + } + ] + } +``` + +### 6.3 Copy the appsettings.json to the container + +Copy the modified appsettings.json file back to the container + +```bash +# For Windows +docker cp /temp/appsettings.json :/app/appsettings.json +``` + +```bash +# For Linux +docker cp /tmp/appsettings.json :/app/appsettings.json +``` + +## Step 7: Set permissions + +Now, you will need to unzip the binaries into the Docker container folder. + +```bash +docker exec -u root -it sh -c "chmod 700 /app/*" +``` + +```bash + docker exec -u root -it sh -c "chmod 777 /app/appsettings.json" +``` + +## Step 8 Restart the Container + +To update the Docker container you need to run the following command to get the Docker Container Id. + +```bash +docker restart +``` +---------- diff --git a/docs/migration-guide-iis.md b/docs/migration-guide-iis.md new file mode 100644 index 000000000..b5a53ef5f --- /dev/null +++ b/docs/migration-guide-iis.md @@ -0,0 +1,98 @@ +# Migrate from previous versions (IIS version) + +To update the IIS site you must need the latest version of the binaries published. + +You can find the preparation steps to publish the binaries in the following [Migration Guide](./migration-guide.md) + +## Update AdminApi (IIS) + +Open Powershell as an Admin to update the files. +Replace the source and destination vars to use your structure. + +Declare the variables + +```bash +$publishFolderPath = "C:\PublishFolder" +$virtualFolderPath = "C:\inetpub\wwwroot\YourVirtualFolder" +``` + +### Step 1: Remove dll files from the virtual folder + +Go to the iis directory for the AdminApi site + +Create a backup of the folder. + +Remove all the dll files from the virtual folder + +```bash +Get-ChildItem -Path $virtualFolderPath -File -Recurse | Where-Object { $_.Name -notmatch '\.sh$|\.config$|appsettings.*\.json$' } | Remove-Item +``` + +### Step 2: Copy binaries to virtual directory + +```bash + + +Get-ChildItem -Path $publishFolderPath -File -Recurse | Where-Object { $_.Name -notmatch 'appsettings.*\.json$|\.config$' } | ForEach-Object { $destPath = $_.FullName.Replace($publishFolderPath, $virtualFolderPath); $destDir = [System.IO.Path]::GetDirectoryName($destPath); if (-not (Test-Path -Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force }; Copy-Item -Path $_.FullName -Destination $destPath } +``` + +### Step 3: Edit appsettings.json file + +Using a text editor add the following lines. + +For the AppSettings section add the parameter + +``` +"PreventDuplicateApplications": "false" +``` + +After the AllowedHosts parameter, add the following section + +``` +"IpRateLimiting": { + "EnableEndpointRateLimiting": true, + "StackBlockedRequests": false, + "RealIpHeader": "X-Real-IP", + "ClientIdHeader": "X-ClientId", + "HttpStatusCode": 429, + "IpWhitelist": [], + "EndpointWhitelist": [], + "GeneralRules": [ + { + "Endpoint": "POST:/Connect/Register", + "Period": "1m", + "Limit": 3 + } + ] + } +``` + +### Step 4: Update permissions + +In some cases it is necessary to update the permissions of the binaries to be executed by the IIS. + +```bash showLineNumbers +$userName = "IIS AppPool\DefaultAppPool" # Change this to your application pool identity + +# Set File System Permissions +$acl = Get-Acl $virtualFolderPath +$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($userName, "ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow") +$acl.SetAccessRule($accessRule) +Set-Acl $virtualFolderPath $acl +``` + +### Step 5: Restart IIS + +To apply the changes you should restart the IIS service or the service Pool. + +You can reset the IIS service. This process will affect the rest io applications. + +``` +iisreset +``` + +Or you can reset the IIS AppPool related to the site. + +``` +Restart-WebAppPool -Name "AdminApiAppPool" +``` diff --git a/docs/migration-guide.md b/docs/migration-guide.md new file mode 100644 index 000000000..1f83cb66b --- /dev/null +++ b/docs/migration-guide.md @@ -0,0 +1,26 @@ +# Migrate from previous versions + +To migrate from a previous version (2.2.0) to a new version (2.2.1) you need to replace the old version binaries with the new ones. + +The steps to migrate to the new version are shown for both an installation in a docker container and an installation in IIS. + +## Building the new version + +### Step 1: Download the latest version + +First, download the latest version and extract the contents of the sources directory to a directory to build the new version binaries. + +### Step 2: Build and publish the new version binaries + +Go to the root directory and execute the build and publish command to generate the new binaries. + +```bash +.\build.ps1 BuildAndPublish +``` + +### Step 3: Update the binaries for your installation + +You can find the steps below to update the binaries for a Docker Container or a IIS installation + +- [Docker Container](./migration-guide-docker.md) +- [IIS Installation](./migration-guide-iis.md) diff --git a/docs/product-info/release-plans/release_plan_Admin API 1.4.2.md b/docs/product-info/release-plans/release_plan_Admin API 1.4.2.md deleted file mode 100644 index 4d827e36e..000000000 --- a/docs/product-info/release-plans/release_plan_Admin API 1.4.2.md +++ /dev/null @@ -1,20 +0,0 @@ -# Admin API 1.4.2 Release Plan - -## Non-Epic Issues -- [ADMINAPI-991] Update libxml (Done) -- [ADMINAPI-988] code owners file for Admin API 1 (Done) -- [ADMINAPI-1073] Release Admin API 1.4.2 (Done) -- [ADMINAPI-1072] Test and QA for Admin API 1.4.2 (Done) -- [ADMINAPI-1056] Investigate Admin API 1.x build errors for EntityFrameworkCore (Done) -- [ADMINAPI-1012] 1.x MSSQL Deployment in Docker Scenarios (Done) -- [ADMINAPI-1003] Ensure Admin API endpoints have paging (limits and offset) implemented - 1.x (Done) -- [ADMINAPI-993] Incorporate the OSSF scorecard in to AdminaApi repos (Done) -- [ADMINAPI-1027] CLONE - various documentation updates needed for 1x adminApi (Done) -- [ADMINAPI-645] Document practice example of creating multiple key/secret pairs from vendor listing file (1.x) (Done) -- [ADMINAPI-1047] When you create an Application with Admin API it will not show in Admin App (Done) -- [ADMINAPI-1026] CLONE (1.x) - Fix OnRelease Workflow to Delete Previous PreRelease *Tags* as well as GH Releases (In QA) -- [ADMINAPI-1007] Using Authority Setting for JWT Issuer (Admin API 1) (Done) -- [ADMINAPI-986] Workflow bug in "Release EdFi.Security.DataAccess Compatibility 5.3" (Done) -- [ADMINAPI-755] An error is thrown in the response instead of a warning message when registration is disabled (Done) -- [ADMINAPI-742] There is no way to set Applications.OdsInstance_OdsInstanceId via the API in Admin API 1.3 (Done) - diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 000000000..1eaaff07b --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,3192 @@ +openapi: 3.0.1 +info: + title: Admin API Documentation + version: v2 +paths: + /v2/resourceClaims: + get: + tags: + - ResourceClaims + summary: Retrieves all resourceClaims. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + required: + - chars + - length + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + required: + - chars + - length + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Resource Claim Id + schema: + required: + - hasValue + - value + type: integer + format: int32 + - name: name + in: query + description: Resource Claim Name + schema: + required: + - chars + - length + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaimModel' + '/v2/resourceClaims/{id}': + get: + tags: + - ResourceClaims + summary: Retrieves a specific resourceClaim based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaimModel' + /v2/vendors: + get: + tags: + - Vendors + summary: Retrieves all vendors. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + required: + - chars + - length + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + required: + - chars + - length + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Vendor/ company id + schema: + required: + - hasValue + - value + type: integer + format: int32 + - name: company + in: query + description: Vendor/ company name + schema: + required: + - chars + - length + type: string + - name: namespacePrefixes + in: query + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + schema: + required: + - chars + - length + type: string + - name: contactName + in: query + description: Vendor contact name + schema: + required: + - chars + - length + type: string + - name: contactEmailAddress + in: query + description: Vendor contact email id + schema: + required: + - chars + - length + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + required: + - length + - longLength + - rank + - syncRoot + - isReadOnly + - isFixedSize + - isSynchronized + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Vendors.VendorModel' + post: + tags: + - Vendors + summary: Creates vendor based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Vendors.AddVendor.Request' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/vendors/{id}': + get: + tags: + - Vendors + summary: Retrieves a specific vendor based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Vendors.VendorModel' + put: + tags: + - Vendors + summary: Updates vendor based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Vendors.EditVendor.Request' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - Vendors + summary: Deletes an existing vendor using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/profiles: + get: + tags: + - Profiles + summary: Retrieves all profiles. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + required: + - chars + - length + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + required: + - chars + - length + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Profile id + schema: + required: + - hasValue + - value + type: integer + format: int32 + - name: name + in: query + description: Profile name + schema: + required: + - chars + - length + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + required: + - length + - longLength + - rank + - syncRoot + - isReadOnly + - isFixedSize + - isSynchronized + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Profiles.ProfileModel' + post: + tags: + - Profiles + summary: Creates profile based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Profiles.AddProfile.AddProfileRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/profiles/{id}': + get: + tags: + - Profiles + summary: Retrieves a specific profile based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Profiles.ProfileDetailsModel' + put: + tags: + - Profiles + summary: Updates profile based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Profiles.EditProfile.EditProfileRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - Profiles + summary: Deletes an existing profile using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstances: + get: + tags: + - OdsInstances + summary: Retrieves all odsInstances. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + required: + - chars + - length + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + required: + - chars + - length + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: List of ODS instance id + schema: + required: + - hasValue + - value + type: integer + format: int32 + - name: name + in: query + description: Ods Instance name + schema: + required: + - chars + - length + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + required: + - length + - longLength + - rank + - syncRoot + - isReadOnly + - isFixedSize + - isSynchronized + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ODSInstances.OdsInstanceModel' + post: + tags: + - OdsInstances + summary: Creates odsInstance based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.OdsInstances.AddOdsInstance.AddOdsInstanceRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstances/{id}': + get: + tags: + - OdsInstances + summary: Retrieves a specific odsInstance based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ODSInstances.OdsInstanceDetailModel' + put: + tags: + - OdsInstances + summary: Updates odsInstance based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.OdsInstances.EditOdsInstance.EditOdsInstanceRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - OdsInstances + summary: Deletes an existing odsInstance using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstanceDerivatives: + get: + tags: + - OdsInstanceDerivatives + summary: Retrieves all odsInstanceDerivatives. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '25' + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + required: + - length + - longLength + - rank + - syncRoot + - isReadOnly + - isFixedSize + - isSynchronized + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.OdsInstanceDerivativeModel' + post: + tags: + - OdsInstanceDerivatives + summary: Creates odsInstanceDerivative based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.AddOdsInstanceDerivative.AddOdsInstanceDerivativeRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstanceDerivatives/{id}': + get: + tags: + - OdsInstanceDerivatives + summary: Retrieves a specific odsInstanceDerivative based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.OdsInstanceDerivativeModel' + put: + tags: + - OdsInstanceDerivatives + summary: Updates odsInstanceDerivative based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.EditOdsInstanceDerivative.EditOdsInstanceDerivativeRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - OdsInstanceDerivatives + summary: Deletes an existing odsInstanceDerivative using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/odsInstanceContexts: + get: + tags: + - OdsInstanceContexts + summary: Retrieves all odsInstanceContexts. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '25' + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + required: + - length + - longLength + - rank + - syncRoot + - isReadOnly + - isFixedSize + - isSynchronized + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.OdsInstanceContext.OdsInstanceContextModel' + post: + tags: + - OdsInstanceContexts + summary: Creates odsInstanceContext based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.OdsInstanceContext.AddOdsInstanceContext.AddOdsInstanceContextRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/odsInstanceContexts/{id}': + get: + tags: + - OdsInstanceContexts + summary: Retrieves a specific odsInstanceContext based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.OdsInstanceContext.OdsInstanceContextModel' + put: + tags: + - OdsInstanceContexts + summary: Updates odsInstanceContext based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.OdsInstanceContext.EditOdsInstanceContext.EditOdsInstanceContextRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - OdsInstanceContexts + summary: Deletes an existing odsInstanceContext using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + '/v2/claimSets/{id}/export': + get: + tags: + - ClaimSets + summary: Retrieves a specific claimSet based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetDetailsModel' + /v2/claimSets: + get: + tags: + - ClaimSets + summary: Retrieves all claimSets. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + required: + - chars + - length + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + required: + - chars + - length + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Claim set id + schema: + required: + - hasValue + - value + type: integer + format: int32 + - name: name + in: query + description: Claim set name + schema: + required: + - chars + - length + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetModel' + post: + tags: + - ClaimSets + summary: Creates claimSet based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.AddClaimSet.AddClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/claimSets/{id}': + get: + tags: + - ClaimSets + summary: Retrieves a specific claimSet based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetDetailsModel' + put: + tags: + - ClaimSets + summary: Updates claimSet based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.EditClaimSet.EditClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - ClaimSets + summary: Deletes an existing claimSet using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + /v2/authorizationStrategies: + get: + tags: + - AuthorizationStrategies + summary: Retrieves all authorizationStrategies. + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + required: + - length + - longLength + - rank + - syncRoot + - isReadOnly + - isFixedSize + - isSynchronized + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.AuthorizationStrategies.AuthorizationStrategyModel' + /v2/applications: + get: + tags: + - Applications + summary: Retrieves all applications. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + required: true + schema: + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + required: true + schema: + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + required: + - chars + - length + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + required: + - chars + - length + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Application id + schema: + required: + - hasValue + - value + type: integer + format: int32 + - name: applicationName + in: query + description: Application name + schema: + required: + - chars + - length + type: string + - name: claimsetName + in: query + description: Claim set name + schema: + required: + - chars + - length + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + required: + - length + - longLength + - rank + - syncRoot + - isReadOnly + - isFixedSize + - isSynchronized + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Applications.ApplicationModel' + post: + tags: + - Applications + summary: Creates application based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Applications.AddApplication.Request' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Applications.ApplicationResult' + '/v2/applications/{id}': + get: + tags: + - Applications + summary: Retrieves a specific application based on the identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Applications.ApplicationModel' + put: + tags: + - Applications + summary: Updates application based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Applications.EditApplication.Request' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - Applications + summary: Deletes an existing application using the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Resource was successfully deleted. + '/v2/odsInstances/{id}/applications': + get: + tags: + - OdsInstances + summary: Retrieves applications assigned to a specific ODS instance based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + required: + - length + - longLength + - rank + - syncRoot + - isReadOnly + - isFixedSize + - isSynchronized + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Applications.ApplicationModel' + '/v2/vendors/{id}/applications': + get: + tags: + - Vendors + summary: Retrieves applications assigned to a specific vendor based on the resource identifier. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + content: + application/json: + schema: + required: + - length + - longLength + - rank + - syncRoot + - isReadOnly + - isFixedSize + - isSynchronized + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Applications.ApplicationModel' + /v2/actions: + get: + tags: + - Actions + summary: Retrieves all actions. + parameters: + - name: offset + in: query + description: Indicates how many items should be skipped before returning results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '0' + - name: limit + in: query + description: Indicates the maximum number of items that should be returned in the results. + schema: + required: + - hasValue + - value + type: integer + format: int32 + default: '25' + - name: orderBy + in: query + description: Indicates the property name by which the results will be sorted. + schema: + required: + - chars + - length + type: string + default: '' + - name: direction + in: query + description: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + schema: + title: Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC). + required: + - chars + - length + enum: + - Ascending + - Descending + type: string + default: Descending + - name: id + in: query + description: Action id + schema: + required: + - hasValue + - value + type: integer + format: int32 + - name: name + in: query + description: Action name + schema: + required: + - chars + - length + type: string + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + required: + - length + - longLength + - rank + - syncRoot + - isReadOnly + - isFixedSize + - isSynchronized + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Actions.ActionModel' + /: + get: + tags: + - Information + summary: Retrieve API informational metadata + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Information.InformationResult' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Information.InformationResult' + /v2/claimSets/copy: + post: + tags: + - ClaimSets + summary: Creates claimSet based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.CopyClaimSet.CopyClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + /v2/claimSets/import: + post: + tags: + - ClaimSets + summary: Creates claimSet based on the supplied values. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ImportClaimSet.Request' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/overrideAuthorizationStrategy': + post: + tags: + - ClaimSets + summary: Creates claimSet based on the supplied values. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditAuthStrategy.OverrideAuthStategyOnClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}/resetAuthorizationStrategies': + post: + tags: + - ClaimSets + summary: Creates claimSet based on the supplied values. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + '/v2/claimSets/{claimSetId}/resourceClaimActions': + post: + tags: + - ClaimSets + summary: Creates claimSet based on the supplied values. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.AddResourceClaimOnClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '201': + description: Created + /connect/register: + post: + tags: + - Connect + summary: Registers new client + description: Registers new client + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + ClientId: + required: + - chars + - length + type: string + description: Client id + ClientSecret: + required: + - chars + - length + type: string + description: Client secret + DisplayName: + required: + - chars + - length + type: string + description: Client display name + encoding: + ClientId: + style: form + ClientSecret: + style: form + DisplayName: + style: form + responses: + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Application registered successfully. + /connect/token: + post: + tags: + - Connect + summary: Retrieves bearer token + description: "\nTo authenticate Swagger requests, execute using \"Authorize\" above, not \"Try It Out\" here." + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + client_id: + type: 'string ' + client_secret: + type: 'string ' + grant_type: + type: 'string ' + scope: + type: string + responses: + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '200': + description: Sign-in successful. + '/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}': + put: + tags: + - ClaimSets + summary: Updates claimSet based on the resource identifier. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.EditResourceClaimOnClaimSetRequest' + required: true + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + delete: + tags: + - ClaimSets + summary: Deletes an existing claimSet using the resource identifier. + parameters: + - name: claimSetId + in: path + required: true + schema: + type: integer + format: int32 + - name: resourceClaimId + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '200': + description: Success + '/v2/applications/{id}/reset-credential': + put: + tags: + - Applications + summary: Reset application credentials. Returns new key and secret. + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + '401': + description: Unauthorized. The request requires authentication + '403': + description: 'Forbidden. The request is authenticated, but not authorized to access this resource' + '409': + description: 'Conflict. The request is authenticated, but it has a conflict with an existing element' + '500': + description: Internal server error. An unhandled error occurred on the server. See the response body for details. + '404': + description: Not found. A resource with given identifier could not be found. + '400': + description: Bad Request. The request was invalid and cannot be completed. See the response body for details. + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Applications.ApplicationResult' +components: + schemas: + EdFi.Ods.AdminApi.Features.Actions.ActionModel: + title: Action + required: + - id + - name + - uri + type: object + properties: + id: + type: integer + format: int32 + name: + required: + - chars + - length + type: string + nullable: true + uri: + required: + - chars + - length + type: string + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.AdminApiError: + title: AdminApiError + required: + - errors + - title + type: object + properties: + title: + required: + - chars + - length + type: string + nullable: true + readOnly: true + errors: + type: array + items: + required: + - chars + - length + type: string + nullable: true + readOnly: true + additionalProperties: false + description: Wrapper schema for all error responses + EdFi.Ods.AdminApi.Features.Applications.AddApplication.Request: + title: AddApplicationRequest + required: + - applicationName + - claimSetName + - educationOrganizationIds + - odsInstanceIds + - vendorId + type: object + properties: + applicationName: + required: + - chars + - length + type: string + description: Application name + vendorId: + type: integer + description: Vendor/ company id + format: int32 + claimSetName: + required: + - chars + - length + type: string + description: Claim set name + profileIds: + type: array + items: + type: integer + format: int32 + description: Profile id + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + description: Education organization ids + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + additionalProperties: false + EdFi.Ods.AdminApi.Features.Applications.ApplicationModel: + title: Application + required: + - applicationName + - claimSetName + - educationOrganizationIds + - id + - odsInstanceIds + - profileIds + - vendorId + type: object + properties: + id: + type: integer + format: int32 + applicationName: + required: + - chars + - length + type: string + nullable: true + claimSetName: + required: + - chars + - length + type: string + nullable: true + educationOrganizationIds: + required: + - item + type: array + items: + type: integer + format: int64 + nullable: true + vendorId: + required: + - hasValue + - value + type: integer + format: int32 + nullable: true + profileIds: + required: + - item + type: array + items: + type: integer + format: int32 + nullable: true + odsInstanceIds: + required: + - item + type: array + items: + type: integer + format: int32 + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.Applications.ApplicationResult: + title: ApplicationKeySecret + required: + - id + - key + - secret + type: object + properties: + id: + type: integer + format: int32 + key: + required: + - chars + - length + type: string + nullable: true + secret: + required: + - chars + - length + type: string + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.Applications.EditApplication.Request: + title: EditApplicationRequest + required: + - applicationName + - claimSetName + - educationOrganizationIds + - id + - odsInstanceIds + - vendorId + type: object + properties: + applicationName: + required: + - chars + - length + type: string + description: Application name + vendorId: + type: integer + description: Vendor/ company id + format: int32 + claimSetName: + required: + - chars + - length + type: string + description: Claim set name + profileIds: + type: array + items: + type: integer + format: int32 + description: Profile id + nullable: true + educationOrganizationIds: + type: array + items: + type: integer + format: int64 + description: Education organization ids + odsInstanceIds: + type: array + items: + type: integer + format: int32 + description: List of ODS instance id + additionalProperties: false + EdFi.Ods.AdminApi.Features.Applications.SimpleApplicationModel: + title: Application + required: + - applicationName + type: object + properties: + applicationName: + required: + - chars + - length + type: string + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.AuthorizationStrategies.AuthorizationStrategyModel: + title: AuthorizationStrategy + required: + - authStrategyId + - authStrategyName + - displayName + type: object + properties: + id: + type: integer + format: int32 + name: + required: + - chars + - length + type: string + nullable: true + displayName: + required: + - chars + - length + type: string + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.ClaimSets.AddClaimSet.AddClaimSetRequest: + title: AddClaimSetRequest + required: + - name + type: object + properties: + name: + required: + - chars + - length + type: string + description: Claim set name + additionalProperties: false + EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetDetailsModel: + title: ClaimSetWithResources + required: + - applications + - id + - isSystemReserved + - name + - resourceClaims + type: object + properties: + id: + type: integer + format: int32 + name: + required: + - chars + - length + type: string + nullable: true + _isSystemReserved: + type: boolean + readOnly: true + _applications: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Applications.SimpleApplicationModel' + nullable: true + readOnly: true + resourceClaims: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetResourceClaimModel' + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetModel: + title: ClaimSet + required: + - applications + - id + - isSystemReserved + - name + type: object + properties: + id: + type: integer + format: int32 + name: + required: + - chars + - length + type: string + nullable: true + _isSystemReserved: + type: boolean + readOnly: true + _applications: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.Applications.SimpleApplicationModel' + nullable: true + readOnly: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetResourceClaimModel: + title: ClaimSetResourceClaim + required: + - actions + - authorizationStrategyOverridesForCRUD + - children + - defaultAuthorizationStrategiesForCRUD + - id + - name + type: object + properties: + id: + type: integer + format: int32 + readOnly: true + name: + required: + - chars + - length + type: string + nullable: true + actions: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaimAction' + nullable: true + _defaultAuthorizationStrategiesForCRUD: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ClaimSetResourceClaimActionAuthStrategies' + nullable: true + readOnly: true + authorizationStrategyOverridesForCRUD: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ClaimSetResourceClaimActionAuthStrategies' + nullable: true + children: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetResourceClaimModel' + description: Children are collection of ResourceClaim + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.ClaimSets.CopyClaimSet.CopyClaimSetRequest: + title: CopyClaimSetRequest + required: + - name + - originalId + type: object + properties: + originalId: + type: integer + description: ClaimSet id to copy + format: int32 + name: + required: + - chars + - length + type: string + description: New claimset name + additionalProperties: false + EdFi.Ods.AdminApi.Features.ClaimSets.EditClaimSet.EditClaimSetRequest: + title: EditClaimSetRequest + required: + - id + - name + type: object + properties: + name: + required: + - chars + - length + type: string + description: Claim set name + additionalProperties: false + EdFi.Ods.AdminApi.Features.ClaimSets.ImportClaimSet.Request: + title: ImportClaimSetRequest + required: + - name + - resourceClaims + type: object + properties: + name: + required: + - chars + - length + type: string + description: Claim set name + resourceClaims: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetResourceClaimModel' + description: Resource Claims + additionalProperties: false + EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaimModel: + title: ResourceClaimModel + required: + - children + - id + - name + - parentId + - parentName + type: object + properties: + id: + type: integer + format: int32 + name: + required: + - chars + - length + type: string + nullable: true + parentId: + required: + - hasValue + - value + type: integer + format: int32 + nullable: true + parentName: + required: + - chars + - length + type: string + nullable: true + children: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaimModel' + description: Children are collection of SimpleResourceClaimModel + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditAuthStrategy.OverrideAuthStategyOnClaimSetRequest: + title: OverrideAuthStategyOnClaimSetRequest + required: + - actionName + - authorizationStrategies + - authStrategyIds + - claimSetId + - resourceClaimId + type: object + properties: + actionName: + required: + - chars + - length + type: string + nullable: true + authorizationStrategies: + type: array + items: + required: + - chars + - length + type: string + description: AuthorizationStrategy Names + additionalProperties: false + EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.AddResourceClaimOnClaimSetRequest: + title: AddResourceClaimActionsOnClaimSetRequest + required: + - claimSetId + - resourceClaimActions + - resourceClaimId + type: object + properties: + resourceClaimId: + type: integer + description: ResourceClaim id + format: int32 + resourceClaimActions: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaimAction' + additionalProperties: false + EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.EditResourceClaimOnClaimSetRequest: + title: EditResourceClaimActionsOnClaimSetRequest + required: + - claimSetId + - resourceClaimActions + - resourceClaimId + type: object + properties: + resourceClaimActions: + required: + - capacity + - count + - item + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaimAction' + additionalProperties: false + EdFi.Ods.AdminApi.Features.Connect.RegisterService.Request: + title: RegisterClientRequest + required: + - clientId + - clientSecret + - displayName + type: object + properties: + clientId: + required: + - chars + - length + type: string + description: Client id + clientSecret: + required: + - chars + - length + type: string + description: Client secret + displayName: + required: + - chars + - length + type: string + description: Client display name + additionalProperties: false + EdFi.Ods.AdminApi.Features.Information.InformationResult: + title: Information + required: + - build + - version + type: object + properties: + version: + required: + - chars + - length + type: string + description: Application version + build: + required: + - chars + - length + type: string + description: Build / release version + additionalProperties: false + EdFi.Ods.AdminApi.Features.ODSInstances.OdsInstanceDetailModel: + title: OdsInstanceDetail + required: + - instanceType + - name + - odsInstanceContexts + - odsInstanceDerivatives + - odsInstanceId + type: object + properties: + id: + type: integer + format: int32 + name: + required: + - chars + - length + type: string + nullable: true + instanceType: + required: + - chars + - length + type: string + nullable: true + odsInstanceContexts: + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.OdsInstanceContext.OdsInstanceContextModel' + nullable: true + odsInstanceDerivatives: + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.OdsInstanceDerivativeModel' + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.ODSInstances.OdsInstanceModel: + title: OdsInstance + required: + - instanceType + - name + - odsInstanceId + type: object + properties: + id: + type: integer + format: int32 + name: + required: + - chars + - length + type: string + nullable: true + instanceType: + required: + - chars + - length + type: string + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.OdsInstanceContext.AddOdsInstanceContext.AddOdsInstanceContextRequest: + title: AddOdsInstanceContextRequest + required: + - contextKey + - contextValue + - odsInstanceId + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance context ODS instance id. + format: int32 + contextKey: + required: + - chars + - length + type: string + description: context key. + contextValue: + required: + - chars + - length + type: string + description: context value. + additionalProperties: false + EdFi.Ods.AdminApi.Features.OdsInstanceContext.EditOdsInstanceContext.EditOdsInstanceContextRequest: + title: EditOdsInstanceContextRequest + required: + - contextKey + - contextValue + - id + - odsInstanceId + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance context ODS instance id. + format: int32 + contextKey: + required: + - chars + - length + type: string + description: context key. + contextValue: + required: + - chars + - length + type: string + description: context value. + additionalProperties: false + EdFi.Ods.AdminApi.Features.OdsInstanceContext.OdsInstanceContextModel: + title: OdsInstanceContext + required: + - contextKey + - contextValue + - odsInstanceContextId + - odsInstanceId + type: object + properties: + id: + type: integer + format: int32 + odsInstanceId: + type: integer + format: int32 + contextKey: + required: + - chars + - length + type: string + nullable: true + contextValue: + required: + - chars + - length + type: string + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.AddOdsInstanceDerivative.AddOdsInstanceDerivativeRequest: + title: AddOdsInstanceDerivativeRequest + required: + - connectionString + - derivativeType + - odsInstanceId + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance derivative ODS instance id. + format: int32 + derivativeType: + required: + - chars + - length + type: string + description: derivative type. + connectionString: + required: + - chars + - length + type: string + description: connection string. + additionalProperties: false + EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.EditOdsInstanceDerivative.EditOdsInstanceDerivativeRequest: + title: EditOdsInstanceDerivativeRequest + required: + - connectionString + - derivativeType + - id + - odsInstanceId + type: object + properties: + odsInstanceId: + type: integer + description: ODS instance derivative ODS instance id. + format: int32 + derivativeType: + required: + - chars + - length + type: string + description: derivative type. + connectionString: + required: + - chars + - length + type: string + description: connection string. + additionalProperties: false + EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.OdsInstanceDerivativeModel: + title: OdsInstanceDerivative + required: + - derivativeType + - id + - odsInstanceId + type: object + properties: + id: + type: integer + format: int32 + odsInstanceId: + required: + - hasValue + - value + type: integer + format: int32 + nullable: true + derivativeType: + required: + - chars + - length + type: string + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.OdsInstances.AddOdsInstance.AddOdsInstanceRequest: + title: AddOdsInstanceRequest + required: + - connectionString + - instanceType + - name + type: object + properties: + name: + required: + - chars + - length + type: string + description: Ods Instance name + instanceType: + required: + - chars + - length + type: string + description: Ods Instance type + connectionString: + required: + - chars + - length + type: string + description: Ods Instance connection string + additionalProperties: false + EdFi.Ods.AdminApi.Features.OdsInstances.EditOdsInstance.EditOdsInstanceRequest: + title: EditOdsInstanceRequest + required: + - connectionString + - id + - instanceType + - name + type: object + properties: + name: + required: + - chars + - length + type: string + description: Ods Instance name + instanceType: + required: + - chars + - length + type: string + description: Ods Instance type + connectionString: + required: + - chars + - length + type: string + description: Ods Instance connection string + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.Profiles.AddProfile.AddProfileRequest: + title: AddProfileRequest + required: + - definition + - name + type: object + properties: + name: + required: + - chars + - length + type: string + description: Profile name + definition: + required: + - chars + - length + type: string + description: Profile definition + additionalProperties: false + example: "{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + EdFi.Ods.AdminApi.Features.Profiles.EditProfile.EditProfileRequest: + title: EditProfileRequest + required: + - definition + - id + - name + type: object + properties: + name: + required: + - chars + - length + type: string + description: Profile name + definition: + required: + - chars + - length + type: string + description: Profile definition + additionalProperties: false + example: "{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + EdFi.Ods.AdminApi.Features.Profiles.ProfileDetailsModel: + title: ProfileDetails + required: + - definition + - id + - name + type: object + properties: + id: + required: + - hasValue + - value + type: integer + format: int32 + nullable: true + name: + required: + - chars + - length + type: string + nullable: true + definition: + required: + - chars + - length + type: string + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.Profiles.ProfileModel: + title: Profile + required: + - id + - name + type: object + properties: + id: + required: + - hasValue + - value + type: integer + format: int32 + nullable: true + name: + required: + - chars + - length + type: string + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Features.Vendors.AddVendor.Request: + title: AddVendorRequest + required: + - company + - contactEmailAddress + - contactName + - namespacePrefixes + type: object + properties: + company: + required: + - chars + - length + type: string + description: Vendor/ company name + namespacePrefixes: + required: + - chars + - length + type: string + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + contactName: + required: + - chars + - length + type: string + description: Vendor contact name + contactEmailAddress: + required: + - chars + - length + type: string + description: Vendor contact email id + additionalProperties: false + EdFi.Ods.AdminApi.Features.Vendors.EditVendor.Request: + title: EditVendorRequest + required: + - company + - contactEmailAddress + - contactName + - id + - namespacePrefixes + type: object + properties: + company: + required: + - chars + - length + type: string + description: Vendor/ company name + namespacePrefixes: + required: + - chars + - length + type: string + description: Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required. + contactName: + required: + - chars + - length + type: string + description: Vendor contact name + contactEmailAddress: + required: + - chars + - length + type: string + description: Vendor contact email id + additionalProperties: false + EdFi.Ods.AdminApi.Features.Vendors.VendorModel: + title: Vendor + required: + - company + - contactEmailAddress + - contactName + - id + - namespacePrefixes + type: object + properties: + id: + required: + - hasValue + - value + type: integer + format: int32 + nullable: true + company: + required: + - chars + - length + type: string + nullable: true + namespacePrefixes: + required: + - chars + - length + type: string + nullable: true + contactName: + required: + - chars + - length + type: string + nullable: true + contactEmailAddress: + required: + - chars + - length + type: string + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.AuthorizationStrategy: + title: ResourceClaimAuthorizationStrategy + required: + - authStrategyId + - authStrategyName + - isInheritedFromParent + type: object + properties: + authStrategyId: + type: integer + format: int32 + authStrategyName: + required: + - chars + - length + type: string + nullable: true + isInheritedFromParent: + type: boolean + additionalProperties: false + EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ClaimSetResourceClaimActionAuthStrategies: + title: ClaimSetResourceClaimActionAuthorizationStrategies + required: + - actionId + - actionName + - authorizationStrategies + type: object + properties: + actionId: + required: + - hasValue + - value + type: integer + format: int32 + nullable: true + actionName: + required: + - chars + - length + type: string + nullable: true + authorizationStrategies: + type: array + items: + $ref: '#/components/schemas/EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.AuthorizationStrategy' + nullable: true + additionalProperties: false + EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaimAction: + title: ResourceClaimAction + required: + - enabled + - name + type: object + properties: + name: + required: + - chars + - length + type: string + nullable: true + enabled: + type: boolean + additionalProperties: false + securitySchemes: + oauth: + type: oauth2 + flows: + clientCredentials: + tokenUrl: http://localhost/connect/token + scopes: + edfi_admin_api/full_access: Unrestricted access to all Admin API endpoints +security: + - oauth: + - api \ No newline at end of file diff --git a/docs/yaml-to-md/adminapi2x.md b/docs/yaml-to-md/adminapi2x.md new file mode 100644 index 000000000..84efeaa6b --- /dev/null +++ b/docs/yaml-to-md/adminapi2x.md @@ -0,0 +1,7317 @@ +--- +title: Admin API Documentation v2 +language_tabs: + - http: HTTP + - python: PYTHON + - csharp: CSHARP +language_clients: + - http: "" + - python: "" + - csharp: "" +toc_footers: [] +includes: [] +search: false +highlight_theme: darkula +headingLevel: 2 + +--- + + + +

Admin API Documentation v2

+ +> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu. + +# Authentication + +- oAuth2 authentication. + + - Flow: clientCredentials + + - Token URL = [http://localhost/connect/token](http://localhost/connect/token) + +|Scope|Scope Description| +|---|---| +|edfi_admin_api/full_access|Unrestricted access to all Admin API endpoints| + +

ResourceClaims

+ +## Retrieves all resourceClaims. + +> Code samples + +```http +GET /v2/resourceClaims HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/resourceClaims', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/resourceClaims"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/resourceClaims` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Resource Claim Id| +|name|query|string|false|Resource Claim Name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + {} + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaimModel](#schemaedfi.ods.adminapi.features.claimsets.resourceclaimmodel)]|false|none|none| +|» ResourceClaimModel|[EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaimModel](#schemaedfi.ods.adminapi.features.claimsets.resourceclaimmodel)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» name|string¦null|true|none|none| +|»» parentId|integer(int32)¦null|true|none|none| +|»» parentName|string¦null|true|none|none| +|»» children|[[EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaimModel](#schemaedfi.ods.adminapi.features.claimsets.resourceclaimmodel)]¦null|true|none|Children are collection of SimpleResourceClaimModel| +|»»» ResourceClaimModel|[EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaimModel](#schemaedfi.ods.adminapi.features.claimsets.resourceclaimmodel)|false|none|none| + + + +## Retrieves a specific resourceClaim based on the identifier. + +> Code samples + +```http +GET /v2/resourceClaims/{id} HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/resourceClaims/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/resourceClaims/{id}"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/resourceClaims/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaimModel](#schemaedfi.ods.adminapi.features.claimsets.resourceclaimmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

Vendors

+ +## Retrieves all vendors. + +> Code samples + +```http +GET /v2/vendors HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/vendors', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/vendors"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/vendors` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Vendor/ company id| +|company|query|string|false|Vendor/ company name| +|namespacePrefixes|query|string|false|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|query|string|false|Vendor contact name| +|contactEmailAddress|query|string|false|Vendor contact email id| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[EdFi.Ods.AdminApi.Features.Vendors.VendorModel](#schemaedfi.ods.adminapi.features.vendors.vendormodel)]|false|none|none| +|» Vendor|[EdFi.Ods.AdminApi.Features.Vendors.VendorModel](#schemaedfi.ods.adminapi.features.vendors.vendormodel)|false|none|none| +|»» id|integer(int32)¦null|true|none|none| +|»» company|string¦null|true|none|none| +|»» namespacePrefixes|string¦null|true|none|none| +|»» contactName|string¦null|true|none|none| +|»» contactEmailAddress|string¦null|true|none|none| + + + +## Creates vendor based on the supplied values. + +> Code samples + +```http +POST /v2/vendors HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/v2/vendors', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + /// Make a dummy request + public async Task MakePostRequest() + { + string url = "/v2/vendors"; + + string json = @"{ + ""company"": ""string"", + ""namespacePrefixes"": ""string"", + ""contactName"": ""string"", + ""contactEmailAddress"": ""string"" +}"; + EdFi.Ods.AdminApi.Features.Vendors.AddVendor.Request content = JsonConvert.DeserializeObject(json); + await PostAsync(content, url); + + + } + + /// Performs a POST Request + public async Task PostAsync(EdFi.Ods.AdminApi.Features.Vendors.AddVendor.Request content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute POST request + HttpResponseMessage response = await Client.PostAsync(url, jsonContent); + } + + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.Vendors.AddVendor.Request content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`POST /v2/vendors` + +> Body parameter + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[EdFi.Ods.AdminApi.Features.Vendors.AddVendor.Request](#schemaedfi.ods.adminapi.features.vendors.addvendor.request)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific vendor based on the identifier. + +> Code samples + +```http +GET /v2/vendors/{id} HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/vendors/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/vendors/{id}"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/vendors/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[EdFi.Ods.AdminApi.Features.Vendors.VendorModel](#schemaedfi.ods.adminapi.features.vendors.vendormodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates vendor based on the resource identifier. + +> Code samples + +```http +PUT /v2/vendors/{id} HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.put('/v2/vendors/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + /// Make a dummy request + public async Task MakePutRequest() + { + int id = 1; + string url = "/v2/vendors/{id}"; + + + string json = @"{ + ""company"": ""string"", + ""namespacePrefixes"": ""string"", + ""contactName"": ""string"", + ""contactEmailAddress"": ""string"" +}"; + EdFi.Ods.AdminApi.Features.Vendors.EditVendor.Request content = JsonConvert.DeserializeObject(json); + var result = await PutAsync(id, content, url); + + + } + + /// Performs a PUT Request + public async Task PutAsync(int id, EdFi.Ods.AdminApi.Features.Vendors.EditVendor.Request content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute PUT request + HttpResponseMessage response = await Client.PutAsync(url + $"/{id}", jsonContent); + + //Return response + return await DeserializeObject(response); + } + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.Vendors.EditVendor.Request content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`PUT /v2/vendors/{id}` + +> Body parameter + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[EdFi.Ods.AdminApi.Features.Vendors.EditVendor.Request](#schemaedfi.ods.adminapi.features.vendors.editvendor.request)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing vendor using the resource identifier. + +> Code samples + +```http +DELETE /v2/vendors/{id} HTTP/1.1 + +``` + +```python +import requests +headers = { + 'Authorization': 'Bearer {access-token}' +} + +r = requests.delete('/v2/vendors/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + + /// Make a dummy request + public async Task MakeDeleteRequest() + { + int id = 1; + string url = "/v2/vendors/{id}"; + + await DeleteAsync(id, url); + } + + /// Performs a DELETE Request + public async Task DeleteAsync(int id, string url) + { + //Execute DELETE request + HttpResponseMessage response = await Client.DeleteAsync(url + $"/{id}"); + + //Return response + await DeserializeObject(response); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`DELETE /v2/vendors/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves applications assigned to a specific vendor based on the resource identifier. + +> Code samples + +```http +GET /v2/vendors/{id}/applications HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/vendors/{id}/applications', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/vendors/{id}/applications"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/vendors/{id}/applications` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[EdFi.Ods.AdminApi.Features.Applications.ApplicationModel](#schemaedfi.ods.adminapi.features.applications.applicationmodel)]|false|none|none| +|» Application|[EdFi.Ods.AdminApi.Features.Applications.ApplicationModel](#schemaedfi.ods.adminapi.features.applications.applicationmodel)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» applicationName|string¦null|true|none|none| +|»» claimSetName|string¦null|true|none|none| +|»» educationOrganizationIds|[integer]¦null|true|none|none| +|»» vendorId|integer(int32)¦null|true|none|none| +|»» profileIds|[integer]¦null|true|none|none| +|»» odsInstanceIds|[integer]¦null|true|none|none| + + + +

Profiles

+ +## Retrieves all profiles. + +> Code samples + +```http +GET /v2/profiles HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/profiles', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/profiles"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/profiles` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Profile id| +|name|query|string|false|Profile name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[EdFi.Ods.AdminApi.Features.Profiles.ProfileModel](#schemaedfi.ods.adminapi.features.profiles.profilemodel)]|false|none|none| +|» Profile|[EdFi.Ods.AdminApi.Features.Profiles.ProfileModel](#schemaedfi.ods.adminapi.features.profiles.profilemodel)|false|none|none| +|»» id|integer(int32)¦null|true|none|none| +|»» name|string¦null|true|none|none| + + + +## Creates profile based on the supplied values. + +> Code samples + +```http +POST /v2/profiles HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/v2/profiles', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + /// Make a dummy request + public async Task MakePostRequest() + { + string url = "/v2/profiles"; + + string json = @"{ + ""name"": ""Test-Profile"", + ""definition"": """" +}"; + EdFi.Ods.AdminApi.Features.Profiles.AddProfile.AddProfileRequest content = JsonConvert.DeserializeObject(json); + await PostAsync(content, url); + + + } + + /// Performs a POST Request + public async Task PostAsync(EdFi.Ods.AdminApi.Features.Profiles.AddProfile.AddProfileRequest content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute POST request + HttpResponseMessage response = await Client.PostAsync(url, jsonContent); + } + + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.Profiles.AddProfile.AddProfileRequest content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`POST /v2/profiles` + +> Body parameter + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[EdFi.Ods.AdminApi.Features.Profiles.AddProfile.AddProfileRequest](#schemaedfi.ods.adminapi.features.profiles.addprofile.addprofilerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific profile based on the identifier. + +> Code samples + +```http +GET /v2/profiles/{id} HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/profiles/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/profiles/{id}"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/profiles/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "definition": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[EdFi.Ods.AdminApi.Features.Profiles.ProfileDetailsModel](#schemaedfi.ods.adminapi.features.profiles.profiledetailsmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates profile based on the resource identifier. + +> Code samples + +```http +PUT /v2/profiles/{id} HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.put('/v2/profiles/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + /// Make a dummy request + public async Task MakePutRequest() + { + int id = 1; + string url = "/v2/profiles/{id}"; + + + string json = @"{ + ""name"": ""Test-Profile"", + ""definition"": """" +}"; + EdFi.Ods.AdminApi.Features.Profiles.EditProfile.EditProfileRequest content = JsonConvert.DeserializeObject(json); + var result = await PutAsync(id, content, url); + + + } + + /// Performs a PUT Request + public async Task PutAsync(int id, EdFi.Ods.AdminApi.Features.Profiles.EditProfile.EditProfileRequest content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute PUT request + HttpResponseMessage response = await Client.PutAsync(url + $"/{id}", jsonContent); + + //Return response + return await DeserializeObject(response); + } + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.Profiles.EditProfile.EditProfileRequest content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`PUT /v2/profiles/{id}` + +> Body parameter + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[EdFi.Ods.AdminApi.Features.Profiles.EditProfile.EditProfileRequest](#schemaedfi.ods.adminapi.features.profiles.editprofile.editprofilerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing profile using the resource identifier. + +> Code samples + +```http +DELETE /v2/profiles/{id} HTTP/1.1 + +``` + +```python +import requests +headers = { + 'Authorization': 'Bearer {access-token}' +} + +r = requests.delete('/v2/profiles/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + + /// Make a dummy request + public async Task MakeDeleteRequest() + { + int id = 1; + string url = "/v2/profiles/{id}"; + + await DeleteAsync(id, url); + } + + /// Performs a DELETE Request + public async Task DeleteAsync(int id, string url) + { + //Execute DELETE request + HttpResponseMessage response = await Client.DeleteAsync(url + $"/{id}"); + + //Return response + await DeserializeObject(response); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`DELETE /v2/profiles/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

OdsInstances

+ +## Retrieves all odsInstances. + +> Code samples + +```http +GET /v2/odsInstances HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/odsInstances', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/odsInstances"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/odsInstances` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|List of ODS instance id| +|name|query|string|false|Ods Instance name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "instanceType": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[EdFi.Ods.AdminApi.Features.ODSInstances.OdsInstanceModel](#schemaedfi.ods.adminapi.features.odsinstances.odsinstancemodel)]|false|none|none| +|» OdsInstance|[EdFi.Ods.AdminApi.Features.ODSInstances.OdsInstanceModel](#schemaedfi.ods.adminapi.features.odsinstances.odsinstancemodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|true|none|none| +|»» instanceType|string¦null|true|none|none| + + + +## Creates odsInstance based on the supplied values. + +> Code samples + +```http +POST /v2/odsInstances HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/v2/odsInstances', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + /// Make a dummy request + public async Task MakePostRequest() + { + string url = "/v2/odsInstances"; + + string json = @"{ + ""name"": ""string"", + ""instanceType"": ""string"", + ""connectionString"": ""string"" +}"; + EdFi.Ods.AdminApi.Features.OdsInstances.AddOdsInstance.AddOdsInstanceRequest content = JsonConvert.DeserializeObject(json); + await PostAsync(content, url); + + + } + + /// Performs a POST Request + public async Task PostAsync(EdFi.Ods.AdminApi.Features.OdsInstances.AddOdsInstance.AddOdsInstanceRequest content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute POST request + HttpResponseMessage response = await Client.PostAsync(url, jsonContent); + } + + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.OdsInstances.AddOdsInstance.AddOdsInstanceRequest content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`POST /v2/odsInstances` + +> Body parameter + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[EdFi.Ods.AdminApi.Features.OdsInstances.AddOdsInstance.AddOdsInstanceRequest](#schemaedfi.ods.adminapi.features.odsinstances.addodsinstance.addodsinstancerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstance based on the identifier. + +> Code samples + +```http +GET /v2/odsInstances/{id} HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/odsInstances/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/odsInstances/{id}"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/odsInstances/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string", + "odsInstanceContexts": [ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } + ], + "odsInstanceDerivatives": [ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[EdFi.Ods.AdminApi.Features.ODSInstances.OdsInstanceDetailModel](#schemaedfi.ods.adminapi.features.odsinstances.odsinstancedetailmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstance based on the resource identifier. + +> Code samples + +```http +PUT /v2/odsInstances/{id} HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.put('/v2/odsInstances/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + /// Make a dummy request + public async Task MakePutRequest() + { + int id = 1; + string url = "/v2/odsInstances/{id}"; + + + string json = @"{ + ""name"": ""string"", + ""instanceType"": ""string"", + ""connectionString"": ""string"" +}"; + EdFi.Ods.AdminApi.Features.OdsInstances.EditOdsInstance.EditOdsInstanceRequest content = JsonConvert.DeserializeObject(json); + var result = await PutAsync(id, content, url); + + + } + + /// Performs a PUT Request + public async Task PutAsync(int id, EdFi.Ods.AdminApi.Features.OdsInstances.EditOdsInstance.EditOdsInstanceRequest content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute PUT request + HttpResponseMessage response = await Client.PutAsync(url + $"/{id}", jsonContent); + + //Return response + return await DeserializeObject(response); + } + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.OdsInstances.EditOdsInstance.EditOdsInstanceRequest content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`PUT /v2/odsInstances/{id}` + +> Body parameter + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[EdFi.Ods.AdminApi.Features.OdsInstances.EditOdsInstance.EditOdsInstanceRequest](#schemaedfi.ods.adminapi.features.odsinstances.editodsinstance.editodsinstancerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstance using the resource identifier. + +> Code samples + +```http +DELETE /v2/odsInstances/{id} HTTP/1.1 + +``` + +```python +import requests +headers = { + 'Authorization': 'Bearer {access-token}' +} + +r = requests.delete('/v2/odsInstances/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + + /// Make a dummy request + public async Task MakeDeleteRequest() + { + int id = 1; + string url = "/v2/odsInstances/{id}"; + + await DeleteAsync(id, url); + } + + /// Performs a DELETE Request + public async Task DeleteAsync(int id, string url) + { + //Execute DELETE request + HttpResponseMessage response = await Client.DeleteAsync(url + $"/{id}"); + + //Return response + await DeserializeObject(response); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`DELETE /v2/odsInstances/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves applications assigned to a specific ODS instance based on the resource identifier. + +> Code samples + +```http +GET /v2/odsInstances/{id}/applications HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/odsInstances/{id}/applications', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/odsInstances/{id}/applications"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/odsInstances/{id}/applications` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[EdFi.Ods.AdminApi.Features.Applications.ApplicationModel](#schemaedfi.ods.adminapi.features.applications.applicationmodel)]|false|none|none| +|» Application|[EdFi.Ods.AdminApi.Features.Applications.ApplicationModel](#schemaedfi.ods.adminapi.features.applications.applicationmodel)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» applicationName|string¦null|true|none|none| +|»» claimSetName|string¦null|true|none|none| +|»» educationOrganizationIds|[integer]¦null|true|none|none| +|»» vendorId|integer(int32)¦null|true|none|none| +|»» profileIds|[integer]¦null|true|none|none| +|»» odsInstanceIds|[integer]¦null|true|none|none| + + + +

OdsInstanceDerivatives

+ +## Retrieves all odsInstanceDerivatives. + +> Code samples + +```http +GET /v2/odsInstanceDerivatives HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/odsInstanceDerivatives', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/odsInstanceDerivatives"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/odsInstanceDerivatives` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.OdsInstanceDerivativeModel](#schemaedfi.ods.adminapi.features.odsinstancederivative.odsinstancederivativemodel)]|false|none|none| +|» OdsInstanceDerivative|[EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.OdsInstanceDerivativeModel](#schemaedfi.ods.adminapi.features.odsinstancederivative.odsinstancederivativemodel)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» odsInstanceId|integer(int32)¦null|true|none|none| +|»» derivativeType|string¦null|true|none|none| + + + +## Creates odsInstanceDerivative based on the supplied values. + +> Code samples + +```http +POST /v2/odsInstanceDerivatives HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/v2/odsInstanceDerivatives', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + /// Make a dummy request + public async Task MakePostRequest() + { + string url = "/v2/odsInstanceDerivatives"; + + string json = @"{ + ""odsInstanceId"": 0, + ""derivativeType"": ""string"", + ""connectionString"": ""string"" +}"; + EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.AddOdsInstanceDerivative.AddOdsInstanceDerivativeRequest content = JsonConvert.DeserializeObject(json); + await PostAsync(content, url); + + + } + + /// Performs a POST Request + public async Task PostAsync(EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.AddOdsInstanceDerivative.AddOdsInstanceDerivativeRequest content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute POST request + HttpResponseMessage response = await Client.PostAsync(url, jsonContent); + } + + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.AddOdsInstanceDerivative.AddOdsInstanceDerivativeRequest content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`POST /v2/odsInstanceDerivatives` + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.AddOdsInstanceDerivative.AddOdsInstanceDerivativeRequest](#schemaedfi.ods.adminapi.features.odsinstancederivative.addodsinstancederivative.addodsinstancederivativerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstanceDerivative based on the identifier. + +> Code samples + +```http +GET /v2/odsInstanceDerivatives/{id} HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/odsInstanceDerivatives/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/odsInstanceDerivatives/{id}"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/odsInstanceDerivatives/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.OdsInstanceDerivativeModel](#schemaedfi.ods.adminapi.features.odsinstancederivative.odsinstancederivativemodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstanceDerivative based on the resource identifier. + +> Code samples + +```http +PUT /v2/odsInstanceDerivatives/{id} HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.put('/v2/odsInstanceDerivatives/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + /// Make a dummy request + public async Task MakePutRequest() + { + int id = 1; + string url = "/v2/odsInstanceDerivatives/{id}"; + + + string json = @"{ + ""odsInstanceId"": 0, + ""derivativeType"": ""string"", + ""connectionString"": ""string"" +}"; + EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.EditOdsInstanceDerivative.EditOdsInstanceDerivativeRequest content = JsonConvert.DeserializeObject(json); + var result = await PutAsync(id, content, url); + + + } + + /// Performs a PUT Request + public async Task PutAsync(int id, EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.EditOdsInstanceDerivative.EditOdsInstanceDerivativeRequest content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute PUT request + HttpResponseMessage response = await Client.PutAsync(url + $"/{id}", jsonContent); + + //Return response + return await DeserializeObject(response); + } + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.EditOdsInstanceDerivative.EditOdsInstanceDerivativeRequest content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`PUT /v2/odsInstanceDerivatives/{id}` + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.EditOdsInstanceDerivative.EditOdsInstanceDerivativeRequest](#schemaedfi.ods.adminapi.features.odsinstancederivative.editodsinstancederivative.editodsinstancederivativerequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstanceDerivative using the resource identifier. + +> Code samples + +```http +DELETE /v2/odsInstanceDerivatives/{id} HTTP/1.1 + +``` + +```python +import requests +headers = { + 'Authorization': 'Bearer {access-token}' +} + +r = requests.delete('/v2/odsInstanceDerivatives/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + + /// Make a dummy request + public async Task MakeDeleteRequest() + { + int id = 1; + string url = "/v2/odsInstanceDerivatives/{id}"; + + await DeleteAsync(id, url); + } + + /// Performs a DELETE Request + public async Task DeleteAsync(int id, string url) + { + //Execute DELETE request + HttpResponseMessage response = await Client.DeleteAsync(url + $"/{id}"); + + //Return response + await DeserializeObject(response); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`DELETE /v2/odsInstanceDerivatives/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

OdsInstanceContexts

+ +## Retrieves all odsInstanceContexts. + +> Code samples + +```http +GET /v2/odsInstanceContexts HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/odsInstanceContexts', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/odsInstanceContexts"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/odsInstanceContexts` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[EdFi.Ods.AdminApi.Features.OdsInstanceContext.OdsInstanceContextModel](#schemaedfi.ods.adminapi.features.odsinstancecontext.odsinstancecontextmodel)]|false|none|none| +|» OdsInstanceContext|[EdFi.Ods.AdminApi.Features.OdsInstanceContext.OdsInstanceContextModel](#schemaedfi.ods.adminapi.features.odsinstancecontext.odsinstancecontextmodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» odsInstanceId|integer(int32)|true|none|none| +|»» contextKey|string¦null|true|none|none| +|»» contextValue|string¦null|true|none|none| + + + +## Creates odsInstanceContext based on the supplied values. + +> Code samples + +```http +POST /v2/odsInstanceContexts HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/v2/odsInstanceContexts', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + /// Make a dummy request + public async Task MakePostRequest() + { + string url = "/v2/odsInstanceContexts"; + + string json = @"{ + ""odsInstanceId"": 0, + ""contextKey"": ""string"", + ""contextValue"": ""string"" +}"; + EdFi.Ods.AdminApi.Features.OdsInstanceContext.AddOdsInstanceContext.AddOdsInstanceContextRequest content = JsonConvert.DeserializeObject(json); + await PostAsync(content, url); + + + } + + /// Performs a POST Request + public async Task PostAsync(EdFi.Ods.AdminApi.Features.OdsInstanceContext.AddOdsInstanceContext.AddOdsInstanceContextRequest content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute POST request + HttpResponseMessage response = await Client.PostAsync(url, jsonContent); + } + + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.OdsInstanceContext.AddOdsInstanceContext.AddOdsInstanceContextRequest content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`POST /v2/odsInstanceContexts` + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[EdFi.Ods.AdminApi.Features.OdsInstanceContext.AddOdsInstanceContext.AddOdsInstanceContextRequest](#schemaedfi.ods.adminapi.features.odsinstancecontext.addodsinstancecontext.addodsinstancecontextrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific odsInstanceContext based on the identifier. + +> Code samples + +```http +GET /v2/odsInstanceContexts/{id} HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/odsInstanceContexts/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/odsInstanceContexts/{id}"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/odsInstanceContexts/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[EdFi.Ods.AdminApi.Features.OdsInstanceContext.OdsInstanceContextModel](#schemaedfi.ods.adminapi.features.odsinstancecontext.odsinstancecontextmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates odsInstanceContext based on the resource identifier. + +> Code samples + +```http +PUT /v2/odsInstanceContexts/{id} HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.put('/v2/odsInstanceContexts/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + /// Make a dummy request + public async Task MakePutRequest() + { + int id = 1; + string url = "/v2/odsInstanceContexts/{id}"; + + + string json = @"{ + ""odsInstanceId"": 0, + ""contextKey"": ""string"", + ""contextValue"": ""string"" +}"; + EdFi.Ods.AdminApi.Features.OdsInstanceContext.EditOdsInstanceContext.EditOdsInstanceContextRequest content = JsonConvert.DeserializeObject(json); + var result = await PutAsync(id, content, url); + + + } + + /// Performs a PUT Request + public async Task PutAsync(int id, EdFi.Ods.AdminApi.Features.OdsInstanceContext.EditOdsInstanceContext.EditOdsInstanceContextRequest content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute PUT request + HttpResponseMessage response = await Client.PutAsync(url + $"/{id}", jsonContent); + + //Return response + return await DeserializeObject(response); + } + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.OdsInstanceContext.EditOdsInstanceContext.EditOdsInstanceContextRequest content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`PUT /v2/odsInstanceContexts/{id}` + +> Body parameter + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[EdFi.Ods.AdminApi.Features.OdsInstanceContext.EditOdsInstanceContext.EditOdsInstanceContextRequest](#schemaedfi.ods.adminapi.features.odsinstancecontext.editodsinstancecontext.editodsinstancecontextrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing odsInstanceContext using the resource identifier. + +> Code samples + +```http +DELETE /v2/odsInstanceContexts/{id} HTTP/1.1 + +``` + +```python +import requests +headers = { + 'Authorization': 'Bearer {access-token}' +} + +r = requests.delete('/v2/odsInstanceContexts/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + + /// Make a dummy request + public async Task MakeDeleteRequest() + { + int id = 1; + string url = "/v2/odsInstanceContexts/{id}"; + + await DeleteAsync(id, url); + } + + /// Performs a DELETE Request + public async Task DeleteAsync(int id, string url) + { + //Execute DELETE request + HttpResponseMessage response = await Client.DeleteAsync(url + $"/{id}"); + + //Return response + await DeserializeObject(response); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`DELETE /v2/odsInstanceContexts/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

ClaimSets

+ +## Retrieves a specific claimSet based on the identifier. + +> Code samples + +```http +GET /v2/claimSets/{id} HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/claimSets/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/claimSets/{id}"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/claimSets/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetDetailsModel](#schemaedfi.ods.adminapi.features.claimsets.claimsetdetailsmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves all claimSets. + +> Code samples + +```http +GET /v2/claimSets HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/claimSets', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/claimSets"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/claimSets` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Claim set id| +|name|query|string|false|Claim set name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetModel](#schemaedfi.ods.adminapi.features.claimsets.claimsetmodel)]|false|none|none| +|» ClaimSet|[EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetModel](#schemaedfi.ods.adminapi.features.claimsets.claimsetmodel)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» name|string¦null|true|none|none| +|»» _isSystemReserved|boolean|false|read-only|none| +|»» _applications|[[EdFi.Ods.AdminApi.Features.Applications.SimpleApplicationModel](#schemaedfi.ods.adminapi.features.applications.simpleapplicationmodel)]¦null|false|read-only|none| +|»»» Application|[EdFi.Ods.AdminApi.Features.Applications.SimpleApplicationModel](#schemaedfi.ods.adminapi.features.applications.simpleapplicationmodel)|false|none|none| +|»»»» applicationName|string¦null|true|none|none| + + + +## Creates claimSet based on the supplied values. + +> Code samples + +```http +POST /v2/claimSets/{claimSetId}/resourceClaimActions HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/v2/claimSets/{claimSetId}/resourceClaimActions', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + /// Make a dummy request + public async Task MakePostRequest() + { + string url = "/v2/claimSets/{claimSetId}/resourceClaimActions"; + + string json = @"{ + ""resourceClaimId"": 0, + ""resourceClaimActions"": [ + { + ""name"": ""string"", + ""enabled"": true + } + ] +}"; + EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.AddResourceClaimOnClaimSetRequest content = JsonConvert.DeserializeObject(json); + await PostAsync(content, url); + + + } + + /// Performs a POST Request + public async Task PostAsync(EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.AddResourceClaimOnClaimSetRequest content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute POST request + HttpResponseMessage response = await Client.PostAsync(url, jsonContent); + } + + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.AddResourceClaimOnClaimSetRequest content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`POST /v2/claimSets/{claimSetId}/resourceClaimActions` + +> Body parameter + +```json +{ + "resourceClaimId": 0, + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|body|body|[EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.AddResourceClaimOnClaimSetRequest](#schemaedfi.ods.adminapi.features.claimsets.resourceclaims.editresourceclaimactions.addresourceclaimonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates claimSet based on the resource identifier. + +> Code samples + +```http +PUT /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId} HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.put('/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + /// Make a dummy request + public async Task MakePutRequest() + { + int id = 1; + string url = "/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}"; + + + string json = @"{ + ""resourceClaimActions"": [ + { + ""name"": ""string"", + ""enabled"": true + } + ] +}"; + EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.EditResourceClaimOnClaimSetRequest content = JsonConvert.DeserializeObject(json); + var result = await PutAsync(id, content, url); + + + } + + /// Performs a PUT Request + public async Task PutAsync(int id, EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.EditResourceClaimOnClaimSetRequest content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute PUT request + HttpResponseMessage response = await Client.PutAsync(url + $"/{id}", jsonContent); + + //Return response + return await DeserializeObject(response); + } + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.EditResourceClaimOnClaimSetRequest content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`PUT /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}` + +> Body parameter + +```json +{ + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| +|body|body|[EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.EditResourceClaimOnClaimSetRequest](#schemaedfi.ods.adminapi.features.claimsets.resourceclaims.editresourceclaimactions.editresourceclaimonclaimsetrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing claimSet using the resource identifier. + +> Code samples + +```http +DELETE /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId} HTTP/1.1 + +``` + +```python +import requests +headers = { + 'Authorization': 'Bearer {access-token}' +} + +r = requests.delete('/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + + /// Make a dummy request + public async Task MakeDeleteRequest() + { + int id = 1; + string url = "/v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}"; + + await DeleteAsync(id, url); + } + + /// Performs a DELETE Request + public async Task DeleteAsync(int id, string url) + { + //Execute DELETE request + HttpResponseMessage response = await Client.DeleteAsync(url + $"/{id}"); + + //Return response + await DeserializeObject(response); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`DELETE /v2/claimSets/{claimSetId}/resourceClaimActions/{resourceClaimId}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|claimSetId|path|integer(int32)|true|none| +|resourceClaimId|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

AuthorizationStrategies

+ +## Retrieves all authorizationStrategies. + +> Code samples + +```http +GET /v2/authorizationStrategies HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/authorizationStrategies', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/authorizationStrategies"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/authorizationStrategies` + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "displayName": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[EdFi.Ods.AdminApi.Features.AuthorizationStrategies.AuthorizationStrategyModel](#schemaedfi.ods.adminapi.features.authorizationstrategies.authorizationstrategymodel)]|false|none|none| +|» AuthorizationStrategy|[EdFi.Ods.AdminApi.Features.AuthorizationStrategies.AuthorizationStrategyModel](#schemaedfi.ods.adminapi.features.authorizationstrategies.authorizationstrategymodel)|false|none|none| +|»» id|integer(int32)|false|none|none| +|»» name|string¦null|false|none|none| +|»» displayName|string¦null|true|none|none| + + + +

Applications

+ +## Retrieves all applications. + +> Code samples + +```http +GET /v2/applications?offset=0&limit=25 HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/applications', params={ + 'offset': '0', 'limit': '25' +}, headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/applications"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/applications` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|true|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|true|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Application id| +|applicationName|query|string|false|Application name| +|claimsetName|query|string|false|Claim set name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[EdFi.Ods.AdminApi.Features.Applications.ApplicationModel](#schemaedfi.ods.adminapi.features.applications.applicationmodel)]|false|none|none| +|» Application|[EdFi.Ods.AdminApi.Features.Applications.ApplicationModel](#schemaedfi.ods.adminapi.features.applications.applicationmodel)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» applicationName|string¦null|true|none|none| +|»» claimSetName|string¦null|true|none|none| +|»» educationOrganizationIds|[integer]¦null|true|none|none| +|»» vendorId|integer(int32)¦null|true|none|none| +|»» profileIds|[integer]¦null|true|none|none| +|»» odsInstanceIds|[integer]¦null|true|none|none| + + + +## Creates application based on the supplied values. + +> Code samples + +```http +POST /v2/applications HTTP/1.1 + +Content-Type: application/json +Accept: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/v2/applications', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + /// Make a dummy request + public async Task MakePostRequest() + { + string url = "/v2/applications"; + + string json = @"{ + ""applicationName"": ""string"", + ""vendorId"": 0, + ""claimSetName"": ""string"", + ""profileIds"": [ + 0 + ], + ""educationOrganizationIds"": [ + 0 + ], + ""odsInstanceIds"": [ + 0 + ] +}"; + EdFi.Ods.AdminApi.Features.Applications.AddApplication.Request content = JsonConvert.DeserializeObject(json); + await PostAsync(content, url); + + + } + + /// Performs a POST Request + public async Task PostAsync(EdFi.Ods.AdminApi.Features.Applications.AddApplication.Request content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute POST request + HttpResponseMessage response = await Client.PostAsync(url, jsonContent); + } + + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.Applications.AddApplication.Request content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`POST /v2/applications` + +> Body parameter + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[EdFi.Ods.AdminApi.Features.Applications.AddApplication.Request](#schemaedfi.ods.adminapi.features.applications.addapplication.request)|true|none| + +> Example responses + +> 201 Response + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|[EdFi.Ods.AdminApi.Features.Applications.ApplicationResult](#schemaedfi.ods.adminapi.features.applications.applicationresult)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves a specific application based on the identifier. + +> Code samples + +```http +GET /v2/applications/{id} HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/applications/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/applications/{id}"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/applications/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[EdFi.Ods.AdminApi.Features.Applications.ApplicationModel](#schemaedfi.ods.adminapi.features.applications.applicationmodel)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Updates application based on the resource identifier. + +> Code samples + +```http +PUT /v2/applications/{id} HTTP/1.1 + +Content-Type: application/json + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.put('/v2/applications/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + /// Make a dummy request + public async Task MakePutRequest() + { + int id = 1; + string url = "/v2/applications/{id}"; + + + string json = @"{ + ""applicationName"": ""string"", + ""vendorId"": 0, + ""claimSetName"": ""string"", + ""profileIds"": [ + 0 + ], + ""educationOrganizationIds"": [ + 0 + ], + ""odsInstanceIds"": [ + 0 + ] +}"; + EdFi.Ods.AdminApi.Features.Applications.EditApplication.Request content = JsonConvert.DeserializeObject(json); + var result = await PutAsync(id, content, url); + + + } + + /// Performs a PUT Request + public async Task PutAsync(int id, EdFi.Ods.AdminApi.Features.Applications.EditApplication.Request content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute PUT request + HttpResponseMessage response = await Client.PutAsync(url + $"/{id}", jsonContent); + + //Return response + return await DeserializeObject(response); + } + + + /// Serialize an object to Json + private StringContent SerializeObject(EdFi.Ods.AdminApi.Features.Applications.EditApplication.Request content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`PUT /v2/applications/{id}` + +> Body parameter + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| +|body|body|[EdFi.Ods.AdminApi.Features.Applications.EditApplication.Request](#schemaedfi.ods.adminapi.features.applications.editapplication.request)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Deletes an existing application using the resource identifier. + +> Code samples + +```http +DELETE /v2/applications/{id} HTTP/1.1 + +``` + +```python +import requests +headers = { + 'Authorization': 'Bearer {access-token}' +} + +r = requests.delete('/v2/applications/{id}', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + + /// Make a dummy request + public async Task MakeDeleteRequest() + { + int id = 1; + string url = "/v2/applications/{id}"; + + await DeleteAsync(id, url); + } + + /// Performs a DELETE Request + public async Task DeleteAsync(int id, string url) + { + //Execute DELETE request + HttpResponseMessage response = await Client.DeleteAsync(url + $"/{id}"); + + //Return response + await DeserializeObject(response); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`DELETE /v2/applications/{id}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Resource was successfully deleted.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Reset application credentials. Returns new key and secret. + +> Code samples + +```http +PUT /v2/applications/{id}/reset-credential HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.put('/v2/applications/{id}/reset-credential', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + + /// Make a dummy request + public async Task MakePutRequest() + { + int id = 1; + string url = "/v2/applications/{id}/reset-credential"; + + + + var result = await PutAsync(id, null, url); + + } + + /// Performs a PUT Request + public async Task PutAsync(int id, undefined content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute PUT request + HttpResponseMessage response = await Client.PutAsync(url + $"/{id}", jsonContent); + + //Return response + return await DeserializeObject(response); + } + + + /// Serialize an object to Json + private StringContent SerializeObject(undefined content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`PUT /v2/applications/{id}/reset-credential` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|integer(int32)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[EdFi.Ods.AdminApi.Features.Applications.ApplicationResult](#schemaedfi.ods.adminapi.features.applications.applicationresult)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found. A resource with given identifier could not be found.|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +

Actions

+ +## Retrieves all actions. + +> Code samples + +```http +GET /v2/actions HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/v2/actions', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/v2/actions"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /v2/actions` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|offset|query|integer(int32)|false|Indicates how many items should be skipped before returning results.| +|limit|query|integer(int32)|false|Indicates the maximum number of items that should be returned in the results.| +|orderBy|query|string|false|Indicates the property name by which the results will be sorted.| +|direction|query|string|false|Indicates whether the result should be sorted in descending order (DESC) or ascending order (ASC).| +|id|query|integer(int32)|false|Action id| +|name|query|string|false|Action name| + +#### Enumerated Values + +|Parameter|Value| +|---|---| +|direction|Ascending| +|direction|Descending| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "uri": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized. The request requires authentication|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden. The request is authenticated, but not authorized to access this resource|None| +|409|[Conflict](https://tools.ietf.org/html/rfc7231#section-6.5.8)|Conflict. The request is authenticated, but it has a conflict with an existing element|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[EdFi.Ods.AdminApi.Features.Actions.ActionModel](#schemaedfi.ods.adminapi.features.actions.actionmodel)]|false|none|none| +|» Action|[EdFi.Ods.AdminApi.Features.Actions.ActionModel](#schemaedfi.ods.adminapi.features.actions.actionmodel)|false|none|none| +|»» id|integer(int32)|true|none|none| +|»» name|string¦null|true|none|none| +|»» uri|string¦null|true|none|none| + + + +

Information

+ +## Retrieve API informational metadata + +> Code samples + +```http +GET / HTTP/1.1 + +Accept: application/json + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + /// Make a dummy request + public async Task MakeGetRequest() + { + string url = "/"; + var result = await GetAsync(url); + } + + /// Performs a GET Request + public async Task GetAsync(string url) + { + //Start the request + HttpResponseMessage response = await Client.GetAsync(url); + + //Validate result + response.EnsureSuccessStatusCode(); + + } + + + + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`GET /` + +> Example responses + +> 200 Response + +```json +{ + "version": "string", + "build": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|[EdFi.Ods.AdminApi.Features.Information.InformationResult](#schemaedfi.ods.adminapi.features.information.informationresult)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|[EdFi.Ods.AdminApi.Features.Information.InformationResult](#schemaedfi.ods.adminapi.features.information.informationresult)| + + + +

Connect

+ +## Registers new client + +> Code samples + +```http +POST /connect/register HTTP/1.1 + +Content-Type: application/x-www-form-urlencoded + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/connect/register', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + /// Make a dummy request + public async Task MakePostRequest() + { + string url = "/connect/register"; + + + await PostAsync(null, url); + + } + + /// Performs a POST Request + public async Task PostAsync(undefined content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute POST request + HttpResponseMessage response = await Client.PostAsync(url, jsonContent); + } + + + + /// Serialize an object to Json + private StringContent SerializeObject(undefined content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`POST /connect/register` + +Registers new client + +> Body parameter + +```yaml +ClientId: string +ClientSecret: string +DisplayName: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» ClientId|body|string|false|Client id| +|» ClientSecret|body|string|false|Client secret| +|» DisplayName|body|string|false|Client display name| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Application registered successfully.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +## Retrieves bearer token + +> Code samples + +```http +POST /connect/token HTTP/1.1 + +Content-Type: application/x-www-form-urlencoded + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.post('/connect/token', headers = headers) + +print(r.json()) + +``` + +```csharp +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +/// <> +/// Example of Http Client +/// <> +public class HttpExample +{ + private HttpClient Client { get; set; } + + /// <> + /// Setup http client + /// <> + public HttpExample() + { + Client = new HttpClient(); + } + + + /// Make a dummy request + public async Task MakePostRequest() + { + string url = "/connect/token"; + + + await PostAsync(null, url); + + } + + /// Performs a POST Request + public async Task PostAsync(undefined content, string url) + { + //Serialize Object + StringContent jsonContent = SerializeObject(content); + + //Execute POST request + HttpResponseMessage response = await Client.PostAsync(url, jsonContent); + } + + + + /// Serialize an object to Json + private StringContent SerializeObject(undefined content) + { + //Serialize Object + string jsonObject = JsonConvert.SerializeObject(content); + + //Create Json UTF8 String Content + return new StringContent(jsonObject, Encoding.UTF8, "application/json"); + } + + /// Deserialize object from request response + private async Task DeserializeObject(HttpResponseMessage response) + { + //Read body + string responseBody = await response.Content.ReadAsStringAsync(); + + //Deserialize Body to object + var result = JsonConvert.DeserializeObject(responseBody); + } +} + +``` + +`POST /connect/token` + +To authenticate Swagger requests, execute using "Authorize" above, not "Try It Out" here. + +> Body parameter + +```yaml +client_id: null +client_secret: null +grant_type: null +scope: string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|object|false|none| +|» client_id|body|string |false|none| +|» client_secret|body|string |false|none| +|» grant_type|body|string |false|none| +|» scope|body|string|false|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Sign-in successful.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request. The request was invalid and cannot be completed. See the response body for details.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error. An unhandled error occurred on the server. See the response body for details.|None| + + + +# Schemas + +

EdFi.Ods.AdminApi.Features.Actions.ActionModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "uri": "string" +} + +``` + +Action + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|name|string¦null|true|none|none| +|uri|string¦null|true|none|none| + +

EdFi.Ods.AdminApi.Features.AdminApiError

+ + + + + + +```json +{ + "title": "string", + "errors": [ + "string" + ] +} + +``` + +AdminApiError + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|title|string¦null|true|read-only|none| +|errors|[string]¦null|true|read-only|none| + +

EdFi.Ods.AdminApi.Features.Applications.AddApplication.Request

+ + + + + + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +AddApplicationRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string|true|none|Application name| +|vendorId|integer(int32)|true|none|Vendor/ company id| +|claimSetName|string|true|none|Claim set name| +|profileIds|[integer]¦null|false|none|Profile id| +|educationOrganizationIds|[integer]|true|none|Education organization ids| +|odsInstanceIds|[integer]|true|none|List of ODS instance id| + +

EdFi.Ods.AdminApi.Features.Applications.ApplicationModel

+ + + + + + +```json +{ + "id": 0, + "applicationName": "string", + "claimSetName": "string", + "educationOrganizationIds": [ + 0 + ], + "vendorId": 0, + "profileIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +Application + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|applicationName|string¦null|true|none|none| +|claimSetName|string¦null|true|none|none| +|educationOrganizationIds|[integer]¦null|true|none|none| +|vendorId|integer(int32)¦null|true|none|none| +|profileIds|[integer]¦null|true|none|none| +|odsInstanceIds|[integer]¦null|true|none|none| + +

EdFi.Ods.AdminApi.Features.Applications.ApplicationResult

+ + + + + + +```json +{ + "id": 0, + "key": "string", + "secret": "string" +} + +``` + +ApplicationKeySecret + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|key|string¦null|true|none|none| +|secret|string¦null|true|none|none| + +

EdFi.Ods.AdminApi.Features.Applications.EditApplication.Request

+ + + + + + +```json +{ + "applicationName": "string", + "vendorId": 0, + "claimSetName": "string", + "profileIds": [ + 0 + ], + "educationOrganizationIds": [ + 0 + ], + "odsInstanceIds": [ + 0 + ] +} + +``` + +EditApplicationRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string|true|none|Application name| +|vendorId|integer(int32)|true|none|Vendor/ company id| +|claimSetName|string|true|none|Claim set name| +|profileIds|[integer]¦null|false|none|Profile id| +|educationOrganizationIds|[integer]|true|none|Education organization ids| +|odsInstanceIds|[integer]|true|none|List of ODS instance id| + +

EdFi.Ods.AdminApi.Features.Applications.SimpleApplicationModel

+ + + + + + +```json +{ + "applicationName": "string" +} + +``` + +Application + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|applicationName|string¦null|true|none|none| + +

EdFi.Ods.AdminApi.Features.AuthorizationStrategies.AuthorizationStrategyModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "displayName": "string" +} + +``` + +AuthorizationStrategy + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|false|none|none| +|displayName|string¦null|true|none|none| + +

EdFi.Ods.AdminApi.Features.ClaimSets.AddClaimSet.AddClaimSetRequest

+ + + + + + +```json +{ + "name": "string" +} + +``` + +AddClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Claim set name| + +

EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetDetailsModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ], + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} + +``` + +ClaimSetWithResources + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|name|string¦null|true|none|none| +|_isSystemReserved|boolean|false|read-only|none| +|_applications|[[EdFi.Ods.AdminApi.Features.Applications.SimpleApplicationModel](#schemaedfi.ods.adminapi.features.applications.simpleapplicationmodel)]¦null|false|read-only|none| +|resourceClaims|[[EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetResourceClaimModel](#schemaedfi.ods.adminapi.features.claimsets.claimsetresourceclaimmodel)]¦null|true|none|none| + +

EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "_isSystemReserved": true, + "_applications": [ + { + "applicationName": "string" + } + ] +} + +``` + +ClaimSet + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|name|string¦null|true|none|none| +|_isSystemReserved|boolean|false|read-only|none| +|_applications|[[EdFi.Ods.AdminApi.Features.Applications.SimpleApplicationModel](#schemaedfi.ods.adminapi.features.applications.simpleapplicationmodel)]¦null|false|read-only|none| + +

EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetResourceClaimModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [] + } + ] +} + +``` + +ClaimSetResourceClaim + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|read-only|none| +|name|string¦null|true|none|none| +|actions|[[EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaimAction](#schemaedfi.ods.adminapi.infrastructure.claimseteditor.resourceclaimaction)]¦null|true|none|none| +|_defaultAuthorizationStrategiesForCRUD|[[EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ClaimSetResourceClaimActionAuthStrategies](#schemaedfi.ods.adminapi.infrastructure.claimseteditor.claimsetresourceclaimactionauthstrategies)]¦null|false|read-only|none| +|authorizationStrategyOverridesForCRUD|[[EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ClaimSetResourceClaimActionAuthStrategies](#schemaedfi.ods.adminapi.infrastructure.claimseteditor.claimsetresourceclaimactionauthstrategies)]¦null|true|none|none| +|children|[[EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetResourceClaimModel](#schemaedfi.ods.adminapi.features.claimsets.claimsetresourceclaimmodel)]¦null|true|none|Children are collection of ResourceClaim| + +

EdFi.Ods.AdminApi.Features.ClaimSets.CopyClaimSet.CopyClaimSetRequest

+ + + + + + +```json +{ + "originalId": 0, + "name": "string" +} + +``` + +CopyClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|originalId|integer(int32)|true|none|ClaimSet id to copy| +|name|string|true|none|New claimset name| + +

EdFi.Ods.AdminApi.Features.ClaimSets.EditClaimSet.EditClaimSetRequest

+ + + + + + +```json +{ + "name": "string" +} + +``` + +EditClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Claim set name| + +

EdFi.Ods.AdminApi.Features.ClaimSets.ImportClaimSet.Request

+ + + + + + +```json +{ + "name": "string", + "resourceClaims": [ + { + "id": 0, + "name": "string", + "actions": [ + { + "name": "string", + "enabled": true + } + ], + "_defaultAuthorizationStrategiesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "authorizationStrategyOverridesForCRUD": [ + { + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] + } + ], + "children": [ + {} + ] + } + ] +} + +``` + +ImportClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Claim set name| +|resourceClaims|[[EdFi.Ods.AdminApi.Features.ClaimSets.ClaimSetResourceClaimModel](#schemaedfi.ods.adminapi.features.claimsets.claimsetresourceclaimmodel)]|true|none|Resource Claims| + +

EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaimModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [ + { + "id": 0, + "name": "string", + "parentId": 0, + "parentName": "string", + "children": [] + } + ] +} + +``` + +ResourceClaimModel + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|name|string¦null|true|none|none| +|parentId|integer(int32)¦null|true|none|none| +|parentName|string¦null|true|none|none| +|children|[[EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaimModel](#schemaedfi.ods.adminapi.features.claimsets.resourceclaimmodel)]¦null|true|none|Children are collection of SimpleResourceClaimModel| + +

EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditAuthStrategy.OverrideAuthStategyOnClaimSetRequest

+ + + + + + +```json +{ + "actionName": "string", + "authorizationStrategies": [ + "string" + ] +} + +``` + +OverrideAuthStategyOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|actionName|string¦null|true|none|none| +|authorizationStrategies|[string]|true|none|AuthorizationStrategy Names| + +

EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.AddResourceClaimOnClaimSetRequest

+ + + + + + +```json +{ + "resourceClaimId": 0, + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} + +``` + +AddResourceClaimActionsOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|resourceClaimId|integer(int32)|true|none|ResourceClaim id| +|resourceClaimActions|[[EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaimAction](#schemaedfi.ods.adminapi.infrastructure.claimseteditor.resourceclaimaction)]|true|none|none| + +

EdFi.Ods.AdminApi.Features.ClaimSets.ResourceClaims.EditResourceClaimActions.EditResourceClaimOnClaimSetRequest

+ + + + + + +```json +{ + "resourceClaimActions": [ + { + "name": "string", + "enabled": true + } + ] +} + +``` + +EditResourceClaimActionsOnClaimSetRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|resourceClaimActions|[[EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaimAction](#schemaedfi.ods.adminapi.infrastructure.claimseteditor.resourceclaimaction)]|true|none|none| + +

EdFi.Ods.AdminApi.Features.Connect.RegisterService.Request

+ + + + + + +```json +{ + "clientId": "string", + "clientSecret": "string", + "displayName": "string" +} + +``` + +RegisterClientRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|clientId|string|true|none|Client id| +|clientSecret|string|true|none|Client secret| +|displayName|string|true|none|Client display name| + +

EdFi.Ods.AdminApi.Features.Information.InformationResult

+ + + + + + +```json +{ + "version": "string", + "build": "string" +} + +``` + +Information + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|version|string|true|none|Application version| +|build|string|true|none|Build / release version| + +

EdFi.Ods.AdminApi.Features.ODSInstances.OdsInstanceDetailModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string", + "odsInstanceContexts": [ + { + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" + } + ], + "odsInstanceDerivatives": [ + { + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" + } + ] +} + +``` + +OdsInstanceDetail + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|true|none|none| +|instanceType|string¦null|true|none|none| +|odsInstanceContexts|[[EdFi.Ods.AdminApi.Features.OdsInstanceContext.OdsInstanceContextModel](#schemaedfi.ods.adminapi.features.odsinstancecontext.odsinstancecontextmodel)]¦null|true|none|none| +|odsInstanceDerivatives|[[EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.OdsInstanceDerivativeModel](#schemaedfi.ods.adminapi.features.odsinstancederivative.odsinstancederivativemodel)]¦null|true|none|none| + +

EdFi.Ods.AdminApi.Features.ODSInstances.OdsInstanceModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "instanceType": "string" +} + +``` + +OdsInstance + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|name|string¦null|true|none|none| +|instanceType|string¦null|true|none|none| + +

EdFi.Ods.AdminApi.Features.OdsInstanceContext.AddOdsInstanceContext.AddOdsInstanceContextRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +AddOdsInstanceContextRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|true|none|ODS instance context ODS instance id.| +|contextKey|string|true|none|context key.| +|contextValue|string|true|none|context value.| + +

EdFi.Ods.AdminApi.Features.OdsInstanceContext.EditOdsInstanceContext.EditOdsInstanceContextRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +EditOdsInstanceContextRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|true|none|ODS instance context ODS instance id.| +|contextKey|string|true|none|context key.| +|contextValue|string|true|none|context value.| + +

EdFi.Ods.AdminApi.Features.OdsInstanceContext.OdsInstanceContextModel

+ + + + + + +```json +{ + "id": 0, + "odsInstanceId": 0, + "contextKey": "string", + "contextValue": "string" +} + +``` + +OdsInstanceContext + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|false|none|none| +|odsInstanceId|integer(int32)|true|none|none| +|contextKey|string¦null|true|none|none| +|contextValue|string¦null|true|none|none| + +

EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.AddOdsInstanceDerivative.AddOdsInstanceDerivativeRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} + +``` + +AddOdsInstanceDerivativeRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|true|none|ODS instance derivative ODS instance id.| +|derivativeType|string|true|none|derivative type.| +|connectionString|string|true|none|connection string.| + +

EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.EditOdsInstanceDerivative.EditOdsInstanceDerivativeRequest

+ + + + + + +```json +{ + "odsInstanceId": 0, + "derivativeType": "string", + "connectionString": "string" +} + +``` + +EditOdsInstanceDerivativeRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|odsInstanceId|integer(int32)|true|none|ODS instance derivative ODS instance id.| +|derivativeType|string|true|none|derivative type.| +|connectionString|string|true|none|connection string.| + +

EdFi.Ods.AdminApi.Features.OdsInstanceDerivative.OdsInstanceDerivativeModel

+ + + + + + +```json +{ + "id": 0, + "odsInstanceId": 0, + "derivativeType": "string" +} + +``` + +OdsInstanceDerivative + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)|true|none|none| +|odsInstanceId|integer(int32)¦null|true|none|none| +|derivativeType|string¦null|true|none|none| + +

EdFi.Ods.AdminApi.Features.OdsInstances.AddOdsInstance.AddOdsInstanceRequest

+ + + + + + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} + +``` + +AddOdsInstanceRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Ods Instance name| +|instanceType|string|true|none|Ods Instance type| +|connectionString|string|true|none|Ods Instance connection string| + +

EdFi.Ods.AdminApi.Features.OdsInstances.EditOdsInstance.EditOdsInstanceRequest

+ + + + + + +```json +{ + "name": "string", + "instanceType": "string", + "connectionString": "string" +} + +``` + +EditOdsInstanceRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Ods Instance name| +|instanceType|string|true|none|Ods Instance type| +|connectionString|string¦null|true|none|Ods Instance connection string| + +

EdFi.Ods.AdminApi.Features.Profiles.AddProfile.AddProfileRequest

+ + + + + + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + +``` + +AddProfileRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Profile name| +|definition|string|true|none|Profile definition| + +

EdFi.Ods.AdminApi.Features.Profiles.EditProfile.EditProfileRequest

+ + + + + + +```json +"{\r\n \"name\": \"Test-Profile\",\r\n \"definition\": \"\"\r\n}" + +``` + +EditProfileRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|Profile name| +|definition|string|true|none|Profile definition| + +

EdFi.Ods.AdminApi.Features.Profiles.ProfileDetailsModel

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "definition": "string" +} + +``` + +ProfileDetails + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|true|none|none| +|name|string¦null|true|none|none| +|definition|string¦null|true|none|none| + +

EdFi.Ods.AdminApi.Features.Profiles.ProfileModel

+ + + + + + +```json +{ + "id": 0, + "name": "string" +} + +``` + +Profile + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|true|none|none| +|name|string¦null|true|none|none| + +

EdFi.Ods.AdminApi.Features.Vendors.AddVendor.Request

+ + + + + + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +AddVendorRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|company|string|true|none|Vendor/ company name| +|namespacePrefixes|string|true|none|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|string|true|none|Vendor contact name| +|contactEmailAddress|string|true|none|Vendor contact email id| + +

EdFi.Ods.AdminApi.Features.Vendors.EditVendor.Request

+ + + + + + +```json +{ + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +EditVendorRequest + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|company|string|true|none|Vendor/ company name| +|namespacePrefixes|string|true|none|Namespace prefix for the vendor. Multiple namespace prefixes can be provided as comma separated list if required.| +|contactName|string|true|none|Vendor contact name| +|contactEmailAddress|string|true|none|Vendor contact email id| + +

EdFi.Ods.AdminApi.Features.Vendors.VendorModel

+ + + + + + +```json +{ + "id": 0, + "company": "string", + "namespacePrefixes": "string", + "contactName": "string", + "contactEmailAddress": "string" +} + +``` + +Vendor + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int32)¦null|true|none|none| +|company|string¦null|true|none|none| +|namespacePrefixes|string¦null|true|none|none| +|contactName|string¦null|true|none|none| +|contactEmailAddress|string¦null|true|none|none| + +

EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.AuthorizationStrategy

+ + + + + + +```json +{ + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true +} + +``` + +ResourceClaimAuthorizationStrategy + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|authStrategyId|integer(int32)|true|none|none| +|authStrategyName|string¦null|true|none|none| +|isInheritedFromParent|boolean|true|none|none| + +

EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ClaimSetResourceClaimActionAuthStrategies

+ + + + + + +```json +{ + "actionId": 0, + "actionName": "string", + "authorizationStrategies": [ + { + "authStrategyId": 0, + "authStrategyName": "string", + "isInheritedFromParent": true + } + ] +} + +``` + +ClaimSetResourceClaimActionAuthorizationStrategies + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|actionId|integer(int32)¦null|true|none|none| +|actionName|string¦null|true|none|none| +|authorizationStrategies|[[EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.AuthorizationStrategy](#schemaedfi.ods.adminapi.infrastructure.claimseteditor.authorizationstrategy)]¦null|true|none|none| + +

EdFi.Ods.AdminApi.Infrastructure.ClaimSetEditor.ResourceClaimAction

+ + + + + + +```json +{ + "name": "string", + "enabled": true +} + +``` + +ResourceClaimAction + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string¦null|true|none|none| +|enabled|boolean|true|none|none| + diff --git a/docs/yaml-to-md/docusaurus-example.png b/docs/yaml-to-md/docusaurus-example.png new file mode 100644 index 000000000..79bf0db2f Binary files /dev/null and b/docs/yaml-to-md/docusaurus-example.png differ diff --git a/docs/yaml-to-md/elements-example-image.png b/docs/yaml-to-md/elements-example-image.png new file mode 100644 index 000000000..6e8be703a Binary files /dev/null and b/docs/yaml-to-md/elements-example-image.png differ diff --git a/docs/yaml-to-md/yaml-to-md.md b/docs/yaml-to-md/yaml-to-md.md new file mode 100644 index 000000000..97800f2c3 --- /dev/null +++ b/docs/yaml-to-md/yaml-to-md.md @@ -0,0 +1,140 @@ +# Admin API documentation +In order to generate the Admin API 2x documentation we need to generate the YAML/JSON file that contains the definition of it. The file is the most important thing if we want to generate the MD files using a library like Docusaurus, widdershins, and so on. + +The Admin API 2x uses the library called Swashbuckle to expose the Swagger/OpenAPI definition, in other words, the YAML/JSON file, but this one is generated only when the application is running. Fortunately, the library provides a CLI that we can use to generate the file passing the assembly. Let's see how we can do that: + +## Install Swashbuckle.AspNetCore.Cli to generate the JSON/YAML + +- Open a command line in the root of the repository and run the following command: +``` +dotnet tool install Swashbuckle.AspNetCore.Cli --version 6.5.0 +``` + +Check the .config/dotnet-tools.json file and verify if you have the section called swashbuckle.aspnetcore.cli +``` +{ + "version": 1, + "isRoot": true, + "tools": { + "swashbuckle.aspnetcore.cli": { + "version": "6.5.0", + "commands": [ + "swagger" + ], + "rollForward": false + } + } +} +``` + +- Build the EdFi.Ods.AdminApi project and it should generate the assembly, the location should be in Application\EdFi.Ods.AdminApi\bin\Debug\net8.0\EdFi.Ods.AdminApi.dll + +- If everything goes well, we can proceed to generate the api description with following command: +``` +dotnet tool run swagger tofile --output ..\..\docs\swagger.yaml --yaml .\bin\Debug\net8.0\EdFi.Ods.AdminApi.dll v2 +``` +For more details check [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore#swashbuckleaspnetcorecli) + +## Generate documentation +The following libraries to generate the documentation were tested + +### widdershins + +- Install the tool globaly +``` +npm install -g widdershins +``` + +- Generate the file using the YAML file executing the following +``` +widdershins --search false --language_tabs 'http:HTTP' 'python:PYTHON' 'csharp:CSHARP' --summary swagger.yaml -o adminapi2x.md +``` +In this case the MD file has sample code in different programming languages + +[Result](https://github.com/Ed-Fi-Alliance-OSS/AdminAPI-2.x/blob/main/docs/yaml-to-md/adminapi2x.md) + +For more details check: [widdershins](https://github.com/Mermade/widdershins) + +### Elements - Web Component + +- Include the web component in an HTML file + +``` + + + + + + Elements in HTML + + + + + + + + + + +``` +![Example](https://github.com/Ed-Fi-Alliance-OSS/AdminAPI-2.x/blob/main/docs/yaml-to-md/elements-example-image.png "Elements Web Component") + +For more details check: [Elements](https://github.com/stoplightio/elements?tab=readme-ov-file#web-component) + +### Docusaurus - docusaurus-openapi-docs + +- Clone the project in the folder. In this case my-website +``` +git clone --depth 1 https://github.com/PaloAltoNetworks/docusaurus-template-openapi-docs.git my-website +``` +- Mode to the folder +``` +cd .\my-website\ +``` +- Install the dependencies +``` +yarn install +``` +- Clean the default documents +``` +yarn docusaurus clean-api-docs all +``` +- Modify the file docusaurus.config.ts, section plugins and set the config +``` +plugins: [ + [ + "docusaurus-plugin-openapi-docs", + { + id: "openapi", + docsPluginId: "classic", + config: { + adminapi2x: { + specPath: "examples/swagger.yaml", + outputDir: "docs/adminapi2x", + downloadUrl: + "https://raw.githubusercontent.com/Ed-Fi-Alliance-OSS/AdminAPI-2.x/ADMINAPI-950/docs/swagger.yaml", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "tag", + }, + } satisfies OpenApiPlugin.Options, + } satisfies Plugin.PluginOptions, + }, + ], +``` +- Generate the documentation file using the following command: +``` +yarn docusaurus gen-api-docs all +``` +- Execute the site and check the results +``` +yarn start +``` + +![Example](https://github.com/Ed-Fi-Alliance-OSS/AdminAPI-2.x/blob/main/docs/yaml-to-md/docusaurus-example.png "Docusaurus") + +For more details check: [Docusaurus](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs) diff --git a/eng/bulk-key-creation/Bulk-EdFiOdsApplications.psm1 b/eng/bulk-key-creation/Bulk-EdFiOdsApplications.psm1 deleted file mode 100644 index 7701bcdf1..000000000 --- a/eng/bulk-key-creation/Bulk-EdFiOdsApplications.psm1 +++ /dev/null @@ -1,270 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Licensed to the Ed-Fi Alliance under one or more agreements. -# The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -# See the LICENSE and NOTICES files in the project root for more information. - -#requires -version 5 -$ErrorActionPreference = "Stop" - -## CONSTANTS -$ADMINAPI_ENDPOINT_VENDOR = "/v1/vendors" -$ADMINAPI_ENDPOINT_APPLICATION = "/v1/applications" -$ADMINAPI_ENDPOINT_OAUTH_URL = "/connect/token" - -function Bulk-EdFiOdsApplications -{ - <# - .SYNOPSIS - EdFi Bulk Application generator. - - .DESCRIPTION - This PowerShell script creates multiple vendors and applications upon execution and stores the generated keys and secrets in a file. - Please remember to securely store keys and secrets. The script utilizes a CSV file as input to specify the vendors and applications to be created. - The script prevents duplicate creation of vendors and applications by skipping existing combinations. - This Script only works for AdminAPI-1.x - - .EXAMPLE - PS c:/> $parameters = @{ - CSVFilePath = "District details.csv" - BaseApiUrl = 'https://localhost/AdminApi' - namespacePrefixes = "uri://ed-fi.org/" - Key = "Your_Admin_Api_User" - Secret = "Your_Admin_Api_Password" - ClaimsetName = "District Hosted SIS Vendor" - LogRootPath = "Logs" - } - PS c:/> Bulk-EdFiOdsApplications @parameters - - Set your apropiate values . - #> - [CmdletBinding()] - param ( - [hashtable] - [Parameter(Mandatory = $true)] - $Config - ) - - Write-Host "BaseApiUrl $($config.BaseApiUrl)" - # Execute\Init task - Init -Config $config -} - -Function Init -{ - [CmdletBinding()] - param ( - [hashtable] - [Parameter(Mandatory = $true)] - $Config - ) - $error.clear() - # Enable Logging - New-Item -ItemType Directory -Force -Path $Config.logRootPath - $date = Get-Date -Format "yyyy-MM-dd-H-m-s" - $logPath = Join-Path -Path $Config.logRootPath -ChildPath "Applications_Log_$date.log" - Start-Transcript -Path $logPath - - Write-Host "*** Initializing Ed-Fi > CSV Processing. ***" - Write-Host $Config.BaseApiUrl - Get-Token $Config - Write-Host "Creating a Json Object and Post Applications" - Create-Applications $Config - Stop-Transcript -} -<# -.SYNOPSIS - function that generates a token -#> -Function Get-Token -{ - [CmdletBinding()] - param ( - [hashtable] - $Config - ) - # extract the requiered parameters from the config file. - $baseApiUrl = $Config.BaseApiUrl - $oAuthUrl = "$baseApiUrl" + $ADMINAPI_ENDPOINT_OAUTH_URL - Write-Host " *** Getting Token ******** " - $formData = @{ - client_id = $Config.Key - client_secret = $Config.Secret - grant_type = 'client_credentials' - scope = 'edfi_admin_api/full_access' - } - Write-Host "token url $oAuthUrl" - $oAuthResponse = Invoke-RestMethod -Uri "$oAuthUrl" -Method Post -Body $formData - $Config.Token = $OAuthResponse.access_token -} - -Function Set-Headers -{ - [CmdletBinding()] - param ( - [hashtable] - [Parameter(Mandatory = $true)] - $Config - ) - $token = $Config.Token - $headers = @{ - "Accept" = "application/json" - "Authorization" = "Bearer $token" - "Content-Type" = "application/json" - } - return $headers -} -<# -.SYNOPSIS - function that extract the Vendor Id from the Header -#> -Function Extract-VendorId($Result) -{ - try { - $location = $Result.Headers.Location | ConvertTo-Json - return ($location.split("/")[-1]) -replace '\"', '' - } - catch { - Write-Host " An error occurred: $_" - Write-Host $_ - } -} - -Function Get-Resource -{ - [CmdletBinding()] - Param( - [hashtable] - [Parameter(Mandatory = $true)] - $Config, - [string] - [Parameter(Mandatory = $true)] - $EndPoint - ) - - $token = $Config.Token - $headers = Set-Headers $Config - $baseApiUrl = $Config.BaseApiUrl - $uri = "$baseApiUrl" + "$EndPoint" + "?offset=0&limit=1000" - try { - $result = Invoke-WebRequest -Method Get -Uri $uri -Headers $headers - return $result - } - catch { - Write-Host "An error occurred: $uri" - Write-Host $_ - } -} - -<# -.SYNOPSIS - function that allows us to post a payload -#> -Function Post-Data -{ - [CmdletBinding()] - Param( - [hashtable] - [Parameter(Mandatory = $true)] - $Config, - [PSCustomObject] - [Parameter(Mandatory = $true)] - $PostPayloadJSON, - [string] - [Parameter(Mandatory = $true)] - $EndPoint - ) - - $headers = Set-Headers $Config - $baseApiUrl = $Config.BaseApiUrl - $uri = "$baseApiUrl" + "$EndPoint" - Write-Host "Uri ***********$uri" - $requestSchema = ConvertTo-Json $PostPayloadJSON - try { - $result = Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -Body $requestSchema - return $result - } - catch { - Write-Host "An error occurred: $uri" - Write-Host "$requestSchema" - Write-Host $_ - } -} -<# -.SYNOPSIS - function that allows us to create and Post payloads for vendors and applications -#> -Function Create-Applications -{ - [CmdletBinding()] - Param( - [hashtable] - [Parameter(Mandatory = $true)] - $Config - ) - Write-Host "Working file '" $Config.CSVFilePath "'" - $vendorsFromOds = @() - $newApplications = @() - $vendorId = 0 - $applicationsCount = 0 - $csv = import-csv -path $Config.CSVFilePath - $Config.CSVResultFile = ($Config.CSVFilePath -replace '.csv', "$(Get-Date -Format "yyyy-MM-dd-H-m") _key_Secret.csv") - $namesPace = $Config.NamesPace - - $vendorsFromOds = ((Get-Resource $Config $ADMINAPI_ENDPOINT_VENDOR).Content | ConvertFrom-Json)."result" - $applicationsFromOds = ((Get-Resource $Config $ADMINAPI_ENDPOINT_APPLICATION).Content | ConvertFrom-Json)."result" - - Import-Csv $Config.CSVFilePath -delimiter "," -Header EducationOrganiztionId,VendorName,ApplicationName,ContactName,ContactEmail,key,secret|Select-Object -Skip 1| - ForEach-Object { - $vendor = $_.Vendorname - $applicationName = $_.ApplicationName - $vendorId = ($vendorsFromOds | Where-object {$_.company -eq $vendor }).vendorId | Select-Object -First 1 - if ($null -eq $vendorId) - { - $objVendors = [PSCustomObject]@{ - company = [System.Security.SecurityElement]::Escape($_.Vendorname) - namespacePrefixes = [System.Security.SecurityElement]::Escape($Config.namespacePrefixes) - contactName = [System.Security.SecurityElement]::Escape($_.contactName) - contactEmailAddress = [System.Security.SecurityElement]::Escape($_.ContactEmail) - } - $vendorResult = Post-Data $Config $objVendors $ADMINAPI_ENDPOINT_VENDOR - $vendorId = Extract-VendorId $vendorResult - Write-Host "A new Vendor was created $vendorId" - } - $applicationId = ($applicationsFromOds | Where-object {$_.applicationName -eq $applicationName -and $_.vendorId -eq $vendorId }).applicationId | Select-Object -First 1 - if ($null -eq $applicationId) - { - $objApplications = [PSCustomObject]@{ - applicationName = [System.Security.SecurityElement]::Escape($applicationName) - vendorId = $vendorId - claimSetName = [System.Security.SecurityElement]::Escape($Config.ClaimsetName) - educationOrganizationIds = @([System.Security.SecurityElement]::Escape($_.EducationOrganiztionId)) - } - - $application = (Post-Data $Config $ObjApplications $ADMINAPI_ENDPOINT_APPLICATION).Content | ConvertFrom-Json - $applicationResult = $Application."result" - Write-Host "The application $applicationName was created" - $newApplications += [PSCustomObject]@{ - ApplicationName = $_.ApplicationName - key = $applicationResult.key - secret = $applicationResult.secret - } - $applicationsCount ++ - }else - { - Write-Host "The application already exist. Skipped Vendor : $vendor, Application : $applicationName" - } - } - if ($applicationsCount -gt 0) - { - $newApplications | Export-Csv -Path $Config.CSVResultFile -NoTypeInformation - Write-Host "*********************************************************************************************************" - Write-Host "*** Find the keys and secrets in the '$($Config.CSVResultFile)' file " - Write-Host "*********************************************************************************************************" - }else - { - Write-Host "*********************************************************************************************************" - Write-Host "*** No applications were created " - Write-Host "*********************************************************************************************************" - } - -} \ No newline at end of file diff --git a/eng/bulk-key-creation/District details.csv b/eng/bulk-key-creation/District details.csv deleted file mode 100644 index d3e9ba8dd..000000000 --- a/eng/bulk-key-creation/District details.csv +++ /dev/null @@ -1 +0,0 @@ -EducationOrganiztionId,VendorName,ApplicationName,ContactName,ContactEmail diff --git a/eng/connection-strings.psm1 b/eng/connection-strings.psm1 index 5181ef831..0d505c80b 100644 --- a/eng/connection-strings.psm1 +++ b/eng/connection-strings.psm1 @@ -66,10 +66,10 @@ function Build-SqlServerConnectionString { } if ($UseIntegratedSecurity) { - return $connectionString + "Integrated Security=true" + return $connectionString + "Integrated Security=true;Encrypt=False;" } - return $connectionString + "User Id=$Username;Password=$Password" + return $connectionString + "User Id=$Username;Password=$Password;Encrypt=False;" } function New-ConnectionString { diff --git a/eng/database-manager.psm1 b/eng/database-manager.psm1 index 02b68c124..39d277921 100644 --- a/eng/database-manager.psm1 +++ b/eng/database-manager.psm1 @@ -6,7 +6,6 @@ #requires -version 5 $ErrorActionPreference = "Stop" -Set-Variable DbDeployVersion -option Constant -value "2.3.10068" Import-Module -Name "$PSScriptRoot/connection-strings.psm1" @@ -57,7 +56,11 @@ IF EXISTS (SELECT 1 FROM sys.databases WHERE name = '$databaseName') GO "@ - Write-Host "Dropping the $databaseName Database." + # Ensure the SqlServer module is installed. Don't re-install if already present. + if (-not (Get-InstalledModule -Name "SqlServer" -ErrorAction SilentlyContinue)) { + Install-Module sqlserver -Force + } + Import-Module sqlserver -Force Invoke-SqlCmd -ConnectionString $masterConnection -Query $dropDatabase } @@ -80,17 +83,23 @@ function Install-EdFiDbDeploy { $exePath = "$ToolsPath/$toolName.exe" - if (Test-Path $exePath) { - $existing = &dotnet tool list --tool-path $ToolsPath | Select-String -Pattern $toolPackageName | Out-String - if ($existing.Contains("$Version")) { - Write-Host "$toolPackageName is already installed" -ForegroundColor DarkGray - return $exePath - } - else { - Write-Host "Uninstalling old version of $toolPackageName" - &dotnet tool uninstall $toolPackageName --tool-path $ToolsPath | Out-Host - } - } + # v6.2 uses Db.Deploy 3.2.27, version 4.1.52 is for ODS 7.x. + # This means we need to uninstall any existing version before installing the requested version. + + Write-Host "Uninstalling old version of $toolPackageName" + &dotnet tool uninstall $toolPackageName --tool-path $ToolsPath | Out-Host + + # if (Test-Path $exePath) { + # $existing = &dotnet tool list --tool-path $ToolsPath | Select-String -Pattern $toolPackageName | Out-String + # if ($existing.Contains("$Version")) { + # Write-Host "$toolPackageName is already installed" -ForegroundColor DarkGray + # return $exePath + # } + # else { + # Write-Host "Uninstalling old version of $toolPackageName" + # &dotnet tool uninstall $toolPackageName --tool-path $ToolsPath | Out-Host + # } + # } Write-Host "Installing $toolPackageName version $Version in $ToolsPath" &dotnet tool install $toolPackageName --version $Version --tool-path $ToolsPath --add-source $NuGetFeed | Out-Host @@ -107,6 +116,10 @@ function Invoke-DbDeploy { [string] $DbDeployExe = ".tools/EdFi.Db.Deploy.exe", + [string] + [Parameter(Mandatory=$true)] + $DbDeployVersion, + [string] [ValidateSet("PostgreSQL", "SqlServer")] $DatabaseEngine = "SqlServer", @@ -122,7 +135,11 @@ function Invoke-DbDeploy { [string[]] [Parameter(Mandatory=$true)] - $FilePaths + $FilePaths, + + [string] + [Parameter(Mandatory=$true)] + $StandardVersion ) # Convert relative to absolute paths @@ -139,13 +156,21 @@ function Invoke-DbDeploy { "-p", ($paths -Join ",") ) + # Only include standardVersion parameter for DbDeployVersion 4.1.52 and later + if ($DbDeployVersion -ne "3.2.27") { + $arguments += @("--standardVersion", $StandardVersion) + } + Write-Host "Executing: $DbDeployExe $(Get-MaskedConnectionString $arguments)" -ForegroundColor Magenta + if ($DbDeployExe -like "*.exe") { + $DbDeployExe = $DbDeployExe -replace "\.exe$", "" + } + &$DbDeployExe @arguments if ($LASTEXITCODE -ne 0) { throw "Execution of EdFi.Db.Deploy failed." } - } function Install-EdFiDatabase { @@ -168,7 +193,7 @@ function Install-EdFiDatabase { [string] [ValidateSet("Admin", "ODS", "Security")] [Parameter(Mandatory=$true)] - $DatabaseType, + $DatabaseType = "Admin", # True if connection string is for a PostgreSQL database. Otherwise for SQL Server. [switch] @@ -203,7 +228,11 @@ function Install-EdFiDatabase { # Hierarchy of directory paths containing database install files. [string[]] [Parameter(Mandatory=$true)] - $FilePaths + $FilePaths, + + [string] + [Parameter(Mandatory=$true)] + $StandardVersion ) $arguments = @{ @@ -228,10 +257,12 @@ function Install-EdFiDatabase { $arguments = @{ DbDeployExe = $dbDeployExe + DbDeployVersion = $DbDeployVersion DatabaseEngine = "SqlServer" DatabaseType = $DatabaseType ConnectionString = $connectionString FilePaths = $FilePaths + StandardVersion = $StandardVersion } if ($ForPostgreSQL) { @@ -273,7 +304,8 @@ function Install-EdFiAdminDatabase { # EdFi.Db.Deploy tool version to use. [string] - $DbDeployVersion = $DbDeployVersion, + [Parameter(Mandatory=$true)] + $DbDeployVersion, # Ed-Fi NuGet feed for tool download. [string] @@ -314,7 +346,11 @@ function Install-EdFiAdminDatabase { # Hierarchy of directory paths containing database install files. [string[]] - $FilePaths + $FilePaths, + + [string] + [Parameter(Mandatory=$true)] + $StandardVersion ) $arguments = @{ @@ -330,6 +366,7 @@ function Install-EdFiAdminDatabase { Username = $Username Password = $Password FilePaths = BuildDefaultFilePathArray -FilePath $FilePaths -RestApiPackagePath $RestApiPackagePath + StandardVersion = $StandardVersion } Install-EdFiDatabase @arguments @@ -343,7 +380,8 @@ function Install-EdFiODSDatabase { # EdFi.Db.Deploy tool version to use. [string] - $DbDeployVersion = $DbDeployVersion, + [Parameter(Mandatory=$true)] + $DbDeployVersion, # Ed-Fi NuGet feed for tool download. [string] @@ -384,7 +422,11 @@ function Install-EdFiODSDatabase { # Hierarchy of directory paths containing database install files. [string[]] - $FilePaths + $FilePaths, + + [string] + [Parameter(Mandatory=$true)] + $StandardVersion ) $arguments = @{ @@ -400,6 +442,7 @@ function Install-EdFiODSDatabase { Username = $Username Password = $Password FilePaths = BuildDefaultFilePathArray -FilePath $FilePaths -RestApiPackagePath $RestApiPackagePath + StandardVersion = $StandardVersion } Install-EdFiDatabase @arguments @@ -413,7 +456,8 @@ function Install-EdFiSecurityDatabase { # EdFi.Db.Deploy tool version to use. [string] - $DbDeployVersion = $DbDeployVersion, + [Parameter(Mandatory=$true)] + $DbDeployVersion, # Ed-Fi NuGet feed for tool download. [string] @@ -454,9 +498,12 @@ function Install-EdFiSecurityDatabase { # Hierarchy of directory paths containing database install files. [string[]] - $FilePaths - ) + $FilePaths, + [string] + [Parameter(Mandatory=$true)] + $StandardVersion + ) $arguments = @{ ToolsPath = $ToolsPath @@ -471,6 +518,7 @@ function Install-EdFiSecurityDatabase { Username = $Username Password = $Password FilePaths = BuildDefaultFilePathArray -FilePath $FilePaths -RestApiPackagePath $RestApiPackagePath + StandardVersion = $StandardVersion } Install-EdFiDatabase @arguments @@ -484,12 +532,18 @@ function Install-AdminApiTables { # EdFi.Db.Deploy tool version to use. [string] - $DbDeployVersion = $DbDeployVersion, + [Parameter(Mandatory=$true)] + $DbDeployVersion, # Ed-Fi NuGet feed for tool download. [string] $NuGetFeed, + [string] + [ValidateSet("Admin", "ODS", "Security")] + [Parameter(Mandatory=$true)] + $DatabaseType = "Admin", + # True if connection string is for a PostgreSQL database. Otherwise for SQL Server. [switch] $ForPostgreSQL, @@ -520,14 +574,19 @@ function Install-AdminApiTables { # Hierarchy of directory paths containing database install files. [string[]] - $FilePath = "$PSScriptRoot/../Application/EdFi.Ods.AdminApi" + $FilePath = "$PSScriptRoot/../Application/EdFi.Ods.AdminApi", + + [string] + [Parameter(Mandatory=$true)] + $StandardVersion + ) $arguments = @{ ToolsPath = $ToolsPath DbDeployVersion = $DbDeployVersion NuGetFeed = $NuGetFeed - DatabaseType = "Admin" + DatabaseType = $DatabaseType ForPostgreSQL = $ForPostgreSQL Server = $Server DatabaseName = $DatabaseName @@ -536,6 +595,7 @@ function Install-AdminApiTables { Username = $Username Password = $Password FilePaths = @( $FilePath ) + StandardVersion = $StandardVersion } Install-EdFiDatabase @arguments @@ -548,7 +608,7 @@ function Invoke-PrepareDatabasesForTesting { param( [string] [Parameter(Mandatory=$true)] - [ValidateSet("EdFi.RestApi.Databases.EFA", "EdFi.Suite3.RestApi.Databases")] + [ValidateSet("EdFi.Suite3.RestApi.Databases", "EdFi.Suite3.RestApi.Databases.Standard.5.2.0")] $RestApiPackageName, [string] @@ -560,7 +620,8 @@ function Invoke-PrepareDatabasesForTesting { # EdFi.Db.Deploy tool version to use. [string] - $DbDeployVersion = $DbDeployVersion, + [Parameter(Mandatory=$true)] + $DbDeployVersion, # Ed-Fi NuGet feed for tool download. [string] @@ -576,7 +637,7 @@ function Invoke-PrepareDatabasesForTesting { $UseIntegratedSecurity, [string] - $DbUser, + $DbUsername, [string] $DbPassword, @@ -585,7 +646,11 @@ function Invoke-PrepareDatabasesForTesting { $PackagesPath = "$PSScriptRoot/.packages", [string] - $ToolsPath = "$PSScriptRoot/.tools" + $ToolsPath = "$PSScriptRoot/.tools", + + [string] + [Parameter(Mandatory=$true)] + $StandardVersion ) Import-Module -Name "$PSScriptRoot/package-manager.psm1" -Force @@ -599,6 +664,7 @@ function Invoke-PrepareDatabasesForTesting { ToolsPath = $ToolsPath RestApiPackagePrerelease = $RestApiPackagePrerelease } + $dbPackagePath = Get-RestApiPackage @arguments $installArguments = @{ @@ -612,6 +678,7 @@ function Invoke-PrepareDatabasesForTesting { Username = $DbUsername Password = $DbPassword RestApiPackagePath = $dbPackagePath + StandardVersion = $StandardVersion } $removeArguments = @{ Server = $DbServer @@ -622,14 +689,11 @@ function Invoke-PrepareDatabasesForTesting { Password = $DbPassword } - Write-Host "Installing the ODS database to $($installArguments.DatabaseName)" -ForegroundColor Cyan - Remove-SqlServerDatabase @removeArguments - Install-EdFiODSDatabase @installArguments - $installArguments.DatabaseName = "EdFi_Security_Test" $removeArguments.DatabaseName = "EdFi_Security_Test" Write-Host "Installing the Security database to $($installArguments.DatabaseName)" -ForegroundColor Cyan Remove-SqlServerDatabase @removeArguments + Install-EdFiSecurityDatabase @installArguments $installArguments.DatabaseName = "EdFi_Admin_Test" @@ -639,6 +703,7 @@ function Invoke-PrepareDatabasesForTesting { Install-EdFiAdminDatabase @installArguments $installArguments.Remove("RestApiPackagePath") + $installArguments.DatabaseType = "Admin" Write-Host "Installing the Admin App tables to $($installArguments.DatabaseName)" -ForegroundColor Cyan Install-AdminApiTables @installArguments } @@ -653,4 +718,4 @@ $exports = @( "Invoke-PrepareDatabasesForTesting" ) -Export-ModuleMember -Function $exports +Export-ModuleMember -Function $exports \ No newline at end of file diff --git a/eng/package-manager.psm1 b/eng/package-manager.psm1 index c955b121e..75b248224 100644 --- a/eng/package-manager.psm1 +++ b/eng/package-manager.psm1 @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: Apache-2.0 # Licensed to the Ed-Fi Alliance under one or more agreements. # The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. # See the LICENSE and NOTICES files in the project root for more information. @@ -92,6 +92,82 @@ function Push-Package { dotnet nuget push $PackageFile --api-key $NuGetApiKey --source $NuGetFeed } +function Test-PackageCache { + param ( + [string] + [Parameter(Mandatory=$true)] + $PackageName, + + [string] + [Parameter(Mandatory=$true)] + $PackageVersion, + + [string] + [Parameter(Mandatory=$true)] + $PackagesPath + ) + + $cacheManifestPath = "$PackagesPath/.package-cache-manifest.json" + $packageKey = "$PackageName-$PackageVersion" + $wildcardPath = "$PackagesPath/$PackageName.$($PackageVersion.Split('-')[0])*" + + # Check if package is already cached + if (Test-Path $cacheManifestPath) { + try { + $cacheManifest = Get-Content $cacheManifestPath | ConvertFrom-Json -AsHashtable + if ($cacheManifest[$packageKey]) { + $existing = Resolve-Path $wildcardPath -ErrorAction SilentlyContinue + if ($existing) { + Write-Host "Package $PackageName version $PackageVersion already cached, skipping download" -ForegroundColor Green + return $true + } + } + } + catch { + # If manifest is corrupted, we'll redownload + Write-Host "Cache manifest corrupted, will redownload packages" -ForegroundColor Yellow + } + } + + return $false +} + +function Update-PackageCache { + param ( + [string] + [Parameter(Mandatory=$true)] + $PackageName, + + [string] + [Parameter(Mandatory=$true)] + $PackageVersion, + + [string] + [Parameter(Mandatory=$true)] + $PackagesPath + ) + + $cacheManifestPath = "$PackagesPath/.package-cache-manifest.json" + $packageKey = "$PackageName-$PackageVersion" + + # Update cache manifest + $cacheManifest = @{} + if (Test-Path $cacheManifestPath) { + try { + $cacheManifest = Get-Content $cacheManifestPath | ConvertFrom-Json -AsHashtable + } + catch { + $cacheManifest = @{} + } + } + $cacheManifest[$packageKey] = @{ + timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + version = $PackageVersion + } + $cacheManifest | ConvertTo-Json | Set-Content $cacheManifestPath + Write-Host "Package $PackageName version $PackageVersion downloaded and cached" -ForegroundColor Cyan +} + function Move-AppCommon { param ( [string] @@ -145,39 +221,50 @@ function Get-RestApiPackage { $ToolsPath = "$PSScriptRoot/.tools" ) - $wildcardPath = "$PackagesPath/$RestApiPackageName.$RestApiPackageVersion*" + # Determine the full package version string including prerelease + $fullPackageVersion = if ($RestApiPackagePrerelease) { "$RestApiPackageVersion-prerelease" } else { $RestApiPackageVersion } - # Remove anything that already exists, so that it is always easy to - # use Resolve-Path with a wildcard to find the installed path without - # having to parse pre-release number of the package. - $existing = Resolve-Path $wildcardPath -ErrorAction SilentlyContinue - if ($existing) { - Remove-Item -Path $existing -Force -ErrorAction SilentlyContinue -Recurse | Out-Null - } + # Check if package is already cached + $needsDownload = -not (Test-PackageCache -PackageName $RestApiPackageName -PackageVersion $fullPackageVersion -PackagesPath $PackagesPath) - New-Item -Path $PackagesPath -ItemType Directory -Force | Out-Null + if ($needsDownload) { + $wildcardPath = "$PackagesPath/$RestApiPackageName.$RestApiPackageVersion*" - $arguments = @( - "install", "$RestApiPackageName", - "-OutputDirectory", "$PackagesPath", - "-Source", "$NuGetFeed" - ) + # Remove anything that already exists, so that it is always easy to + # use Resolve-Path with a wildcard to find the installed path without + # having to parse pre-release number of the package. + $existing = Resolve-Path $wildcardPath -ErrorAction SilentlyContinue + if ($existing) { + Remove-Item -Path $existing -Force -ErrorAction SilentlyContinue -Recurse | Out-Null + } - if ($RestApiPackagePrerelease) { - $arguments += "-Prerelease" - } - else { - $arguments += "-Version" - $arguments += "$RestApiPackageVersion" - } + New-Item -Path $PackagesPath -ItemType Directory -Force | Out-Null + + $arguments = @( + "install", "$RestApiPackageName", + "-OutputDirectory", "$PackagesPath", + "-Source", "$NuGetFeed" + ) - Write-Host "Executing: nuget $arguments" -ForegroundColor Magenta - nuget @arguments | Out-Null + if ($RestApiPackagePrerelease) { + $arguments += "-Prerelease" + } + else { + $arguments += "-Version" + $arguments += "$RestApiPackageVersion" + } - if ($LASTEXITCODE -ne 0) { - throw "NuGet package install failed for RestApi.Databases" + Write-Host "Executing: nuget $arguments" -ForegroundColor Magenta + nuget @arguments | Out-Null + + if ($LASTEXITCODE -ne 0) { + throw "NuGet package install failed for RestApi.Databases" + } + + Update-PackageCache -PackageName $RestApiPackageName -PackageVersion $fullPackageVersion -PackagesPath $PackagesPath } + $wildcardPath = "$PackagesPath/$RestApiPackageName.$RestApiPackageVersion*" return (Resolve-Path $wildcardPath) } @@ -206,33 +293,41 @@ function Add-AppCommon { $ToolsPath = "$PSScriptRoot/.tools" ) - $wildcardPath = "$PackagesPath/$AppCommonPackageName.$AppCommonPackageVersion*" + # Check if package is already cached + $needsDownload = -not (Test-PackageCache -PackageName $AppCommonPackageName -PackageVersion $AppCommonPackageVersion -PackagesPath $PackagesPath) - # Remove anything that already exists, so that it is always easy to - # use Resolve-Path with a wildcard to find the installed path without - # having to parse pre-release number of the package. - $existing = Resolve-Path $wildcardPath -ErrorAction SilentlyContinue - if ($existing) { - Remove-Item -Path $existing -Force -ErrorAction SilentlyContinue -Recurse | Out-Null - } + if ($needsDownload) { + $wildcardPath = "$PackagesPath/$AppCommonPackageName.$AppCommonPackageVersion*" + + # Remove anything that already exists, so that it is always easy to + # use Resolve-Path with a wildcard to find the installed path without + # having to parse pre-release number of the package. + $existing = Resolve-Path $wildcardPath -ErrorAction SilentlyContinue + if ($existing) { + Remove-Item -Path $existing -Force -ErrorAction SilentlyContinue -Recurse | Out-Null + } - New-Item -Path $PackagesPath -ItemType Directory -Force | Out-Null + New-Item -Path $PackagesPath -ItemType Directory -Force | Out-Null - $parameters = @( - "install", $AppCommonPackageName, - "-source", $NuGetFeed, - "-outputDirectory", $PackagesPath - "-version", $AppCommonPackageVersion - ) + $parameters = @( + "install", $AppCommonPackageName, + "-source", $NuGetFeed, + "-outputDirectory", $PackagesPath + "-version", $AppCommonPackageVersion + ) - Write-Host "Downloading AppCommon" - Write-Host -ForegroundColor Magenta "Executing nuget: $parameters" - nuget $parameters | Out-Null + Write-Host "Downloading AppCommon" + Write-Host -ForegroundColor Magenta "Executing nuget: $parameters" + nuget $parameters | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "NuGet package install failed for AppCommon" + if ($LASTEXITCODE -ne 0) { + throw "NuGet package install failed for AppCommon" + } + + Update-PackageCache -PackageName $AppCommonPackageName -PackageVersion $AppCommonPackageVersion -PackagesPath $PackagesPath } + $wildcardPath = "$PackagesPath/$AppCommonPackageName.$AppCommonPackageVersion*" $appCommonDirectory = Resolve-Path $wildcardPath | Select-Object -Last 1 Move-AppCommon $appCommonDirectory $DestinationPath @@ -242,7 +337,9 @@ $functions = @( "Install-NugetCli", "Get-RestApiPackage", "Push-Package", - "Add-AppCommon" + "Add-AppCommon", + "Test-PackageCache", + "Update-PackageCache" ) -Export-ModuleMember -Function $functions \ No newline at end of file +Export-ModuleMember -Function $functions diff --git a/eng/run-dbup-migrations.ps1 b/eng/run-dbup-migrations.ps1 index 4059bf10f..1652f50fe 100644 --- a/eng/run-dbup-migrations.ps1 +++ b/eng/run-dbup-migrations.ps1 @@ -8,6 +8,7 @@ Param( $config = @{ + "databaseType" = "Admin" "engine" = "SqlServer" "databaseServer" = "(local)" "databasePort" = "" @@ -15,6 +16,7 @@ Param( "databasePassword" = "" "useIntegratedSecurity" = $true "adminDatabaseName" = "EdFi_Admin" + "securityDatabaseName" = "EdFi_Security" } ) @@ -22,8 +24,10 @@ $ErrorActionPreference = "Stop" Import-Module -Name "$PSScriptRoot/database-manager.psm1" -Force +# Admin database $arguments = @{ ToolsPath = ".tools" + DatabaseType = $config.databaseType ForPostgreSQL = "postgresql" -eq $config.engine.ToLower() Server = $config.databaseServer DatabaseName = $config.adminDatabaseName @@ -40,3 +44,24 @@ Install-AdminApiTables @arguments $arguments.DatabaseName = "EdFi_Admin_Test" Write-Output "Installing the Admin API tables to $($arguments.DatabaseName)" Install-AdminApiTables @arguments + +# Security database +$arguments = @{ + ToolsPath = ".tools" + DatabaseType = "Security" + ForPostgreSQL = "postgresql" -eq $config.engine.ToLower() + Server = $config.databaseServer + DatabaseName = $config.securityDatabaseName + Port = $config.databasePort + UseIntegratedSecurity = $config.useIntegratedSecurity + Username = $config.databaseUser + Password = $config.databasePassword + NuGetFeed = "https://pkgs.dev.azure.com/ed-fi-alliance/Ed-Fi-Alliance-OSS/_packaging/EdFi/nuget/v3/index.json" +} + +Write-Output "Installing the Admin API tables to $($arguments.DatabaseName)" +Install-AdminApiTables @arguments + +$arguments.DatabaseName = "EdFi_Security_Test" +Write-Output "Installing the Admin API tables to $($arguments.DatabaseName)" +Install-AdminApiTables @arguments