From 7bc15713b9a95e1c708bcfedf752f33799b99acf Mon Sep 17 00:00:00 2001 From: Pavol Zbell Date: Sat, 6 Feb 2021 11:51:23 +0100 Subject: [PATCH] Refactor IAM identity and UPVS SSO endpoints --- INSTALL.md | 2 +- app/controllers/api_controller.rb | 1 + app/controllers/iam/identities_controller.rb | 2 +- app/controllers/upvs_controller.rb | 25 ++-- app/services/environment.rb | 2 +- app/views/iam/_affix.json.jbuilder | 4 + app/views/iam/_corporate_body.json.jbuilder | 30 +++++ .../iam/_corporate_body_name.json.jbuilder | 1 + app/views/iam/_enumeration.json.jbuilder | 5 + app/views/iam/_identity.json.jbuilder | 114 ++++++++++++++++++ app/views/iam/_natural_person.json.jbuilder | 49 ++++++++ .../iam/_natural_person_name.json.jbuilder | 19 +++ app/views/iam/identities/search.json.jbuilder | 2 +- app/views/iam/identities/show.json.jbuilder | 2 +- config/routes.rb | 3 +- public/openapi.yaml | 70 +++++++---- spec/requests/api/iam_spec.rb | 2 +- spec/requests/api/sktalk_spec.rb | 18 +-- spec/requests/api/upvs_spec.rb | 82 +++++++++++-- spec/requests/sso/upvs_spec.rb | 2 +- spec/services/obo_token_authenticator_spec.rb | 2 +- spec/support/api_requests.rb | 18 +-- spec/support/authenticity_tokens.rb | 4 + 23 files changed, 396 insertions(+), 63 deletions(-) create mode 100644 app/views/iam/_affix.json.jbuilder create mode 100644 app/views/iam/_corporate_body.json.jbuilder create mode 100644 app/views/iam/_corporate_body_name.json.jbuilder create mode 100644 app/views/iam/_enumeration.json.jbuilder create mode 100644 app/views/iam/_identity.json.jbuilder create mode 100644 app/views/iam/_natural_person.json.jbuilder create mode 100644 app/views/iam/_natural_person_name.json.jbuilder diff --git a/INSTALL.md b/INSTALL.md index 62e2c406..a20b1054 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,4 +1,4 @@ -**Inštalačná príručka popisuje komponent verzie** [2.0.0](https://github.com/slovensko-digital/slovensko-sk-api/releases/tag/v2.0.0), uistite sa, že čítate príručku [verzie komponentu](https://github.com/slovensko-digital/slovensko-sk-api/releases), ktorý používate. +**Inštalačná príručka popisuje komponent verzie [2.0.0](https://github.com/slovensko-digital/slovensko-sk-api/releases/tag/v2.0.0), uistite sa, že čítate príručku [verzie komponentu](https://github.com/slovensko-digital/slovensko-sk-api/releases), ktorý používate.** # slovensko.sk API - inštalačná príručka diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 7fc25444..1a188164 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -62,6 +62,7 @@ def self.rescue_from_soap_fault(&handler) private + # TODO replace **options with ... when on ruby 2.7 def authenticate(**options) self.upvs_identity = Environment.api_token_authenticator.verify_token(authenticity_token, options) end diff --git a/app/controllers/iam/identities_controller.rb b/app/controllers/iam/identities_controller.rb index 1bb4ba84..e0ab8c15 100644 --- a/app/controllers/iam/identities_controller.rb +++ b/app/controllers/iam/identities_controller.rb @@ -1,7 +1,7 @@ class Iam::IdentitiesController < ApiController include Pagination - before_action { authenticate(allow_sub: true, allow_obo_token: true, require_obo_token_scope: action_scope) } + before_action { authenticate(allow_sub: true) } before_action(only: :search) { set_page } before_action(only: :search) { set_per_page(default: 10, range: 10..100) } diff --git a/app/controllers/upvs_controller.rb b/app/controllers/upvs_controller.rb index bcac1e06..e3c7b403 100644 --- a/app/controllers/upvs_controller.rb +++ b/app/controllers/upvs_controller.rb @@ -1,8 +1,9 @@ class UpvsController < ApiController - skip_before_action(:verify_request_body, except: :assertion) - skip_before_action(:verify_format) + skip_before_action(:verify_request_body, except: [:assertion, :identity]) + skip_before_action(:verify_format, except: :identity) before_action(only: :assertion) { respond_to(:saml) } + before_action(only: [:assertion, :identity]) { authenticate(allow_obo_token: true, require_obo_token_scope: action_scope) } def login session[:login_callback_url] = fetch_callback_url(Environment.login_callback_urls) @@ -17,12 +18,6 @@ def callback redirect_to callback_url_with_token(session[:login_callback_url], token) end - def assertion - _, assertion = Environment.api_token_authenticator.verify_token(authenticity_token, allow_obo_token: true) - - render content_type: Mime[:saml], plain: assertion - end - def logout if params[:SAMLRequest] redirect_to "/auth/saml/slo?#{slo_request_params.to_query}" @@ -36,12 +31,22 @@ def logout end end + def assertion + render content_type: Mime[:saml], plain: upvs_identity[:obo] + end + + def identity + render partial: 'iam/identity', locals: { identity: iam_repository(upvs_identity).identity(obo_subject_id(upvs_identity[:obo])) } + end + include CallbackHelper CallbackError = Class.new(StandardError) rescue_from(CallbackError) { |error| render_bad_request(error.message, :callback) } + rescue_from(sk.gov.schemas.identity.service._1_7.GetIdentityFault) { |error| render_bad_request(:invalid, :identity_id, upvs_fault(error)) } + private def fetch_callback_url(registered_urls) @@ -63,4 +68,8 @@ def slo_request_params def slo_response_params(redirect_url) params.permit(:SAMLResponse, :SigAlg, :Signature).merge(RelayState: redirect_url) end + + def obo_subject_id(assertion) + Nokogiri::XML(assertion).at_xpath('//saml:Attribute[@Name="SubjectID"]/saml:AttributeValue').content + end end diff --git a/app/services/environment.rb b/app/services/environment.rb index 386ed2e6..ea70a686 100644 --- a/app/services/environment.rb +++ b/app/services/environment.rb @@ -45,7 +45,7 @@ def obo_token_assertion_store end def obo_token_scopes - @obo_token_scopes ||= ['iam/identities/show', 'iam/identities/search', 'sktalk/receive', 'sktalk/receive_and_save_to_outbox', 'sktalk/save_to_outbox'] + @obo_token_scopes ||= ['sktalk/receive', 'sktalk/receive_and_save_to_outbox', 'sktalk/save_to_outbox', 'upvs/assertion', 'upvs/identity'] end # RedisCacheStore ignores standard errors diff --git a/app/views/iam/_affix.json.jbuilder b/app/views/iam/_affix.json.jbuilder new file mode 100644 index 00000000..e6bcc593 --- /dev/null +++ b/app/views/iam/_affix.json.jbuilder @@ -0,0 +1,4 @@ +return json.nil! unless affix + +json.type affix.type.underscore.remove('_title').tap { |t| t.replace('military') if t == 'form_of_address' } +json.value affix.value diff --git a/app/views/iam/_corporate_body.json.jbuilder b/app/views/iam/_corporate_body.json.jbuilder new file mode 100644 index 00000000..5c61e693 --- /dev/null +++ b/app/views/iam/_corporate_body.json.jbuilder @@ -0,0 +1,30 @@ +json.cin identity.id.find { |id| id.identifier_type.id == '7' }&.identifier_value +json.tin identity.id.find { |id| id.identifier_type.id == '8' }&.identifier_value + +json.organization_id corporate_body.org_id +json.organization_units corporate_body.organization_unit + +json.partial! 'iam/corporate_body_name', corporate_body: corporate_body +json.alternative_names corporate_body.corporate_body_alternative_name + +json.legal_form { json.partial! 'iam/enumeration', value: corporate_body.legal_form } +json.legal_facts corporate_body.other_legal_facts + +json.activities corporate_body.activities + +# TODO +# json.bank_connections corporate_body.bank_connection do |c| +# end + +# TODO +# json.stakeholders corporate_body.stakeholder do |s| +# end + +# TODO +# corporate_body.sid +# corporate_body.equity +# corporate_body.suspension + +json.established_on corporate_body.establishment.to_s.to_date +json.terminated_on corporate_body.termination.to_s.to_date +json.updated_on corporate_body.date_of_status_change.to_s.to_date diff --git a/app/views/iam/_corporate_body_name.json.jbuilder b/app/views/iam/_corporate_body_name.json.jbuilder new file mode 100644 index 00000000..4c0b1bdd --- /dev/null +++ b/app/views/iam/_corporate_body_name.json.jbuilder @@ -0,0 +1 @@ +json.name corporate_body.corporate_body_name diff --git a/app/views/iam/_enumeration.json.jbuilder b/app/views/iam/_enumeration.json.jbuilder new file mode 100644 index 00000000..2a03b1f5 --- /dev/null +++ b/app/views/iam/_enumeration.json.jbuilder @@ -0,0 +1,5 @@ +return json.nil! unless value + +json.id value.id +json.name value.title_sk +json.description value.desc diff --git a/app/views/iam/_identity.json.jbuilder b/app/views/iam/_identity.json.jbuilder new file mode 100644 index 00000000..64a52599 --- /dev/null +++ b/app/views/iam/_identity.json.jbuilder @@ -0,0 +1,114 @@ +json.ids identity.general_data.egov_identifier do |egov_identifier| + json.type sector = egov_identifier.sector_identifier.downcase + json.value egov_identifier.identifier.tap { |id| id.downcase! if sector == 'sector_upvs' } +end + +json.uri identity.general_data.uri +json.en identity.upvs_attributes.edesk_number +json.type identity.general_data.identity_type.value.downcase +json.status identity.upvs_attributes.identity_status.value.downcase +json.name identity.general_data.formatted_name +json.suffix identity.general_data.suffix + +json.various_ids identity.id do |id| + json.type { json.partial! 'iam/enumeration', value: id.identifier_type } + json.value id.identifier_value + json.specified id.is_specified +end + +json.upvs do + json.edesk_number identity.upvs_attributes.edesk_number + json.edesk_status identity.upvs_attributes.edesk_status&.value&.downcase + json.edesk_remote_uri identity.upvs_attributes.edesk_remote_uri + json.edesk_cuet_delivery_enabled identity.upvs_attributes.is_edesk_cuet_enabled + json.edesk_delivery_limited identity.upvs_attributes.is_edesk_delivery_limited + + json.enotify_preferred_channel identity.upvs_attributes.enotify_preferred_channel&.value&.downcase + json.enotify_preferred_calendar identity.upvs_attributes.enotify_preferred_calendar&.downcase + json.enotify_emergency_allowed identity.upvs_attributes.is_enotify_emergency_allowed + json.enotify_email_allowed identity.upvs_attributes.is_enotify_email_allowed + json.enotify_sms_allowed identity.upvs_attributes.is_enotify_sms_allowed + + # TODO + # identity.upvs_attributes.organizacna_zlozka_ovm + # identity.upvs_attributes.issuer_foreign_eid + # identity.upvs_attributes.location + # identity.upvs_attributes.location_activated + + json.preferred_language identity.upvs_attributes.preferred_language&.value&.downcase + + json.re_iam_identity_id identity.upvs_attributes.re_identity_id +end + +if identity.corporate_body + json.corporate_body { json.partial! 'iam/corporate_body', identity: identity, corporate_body: identity.corporate_body } +end + +if identity.physical_person + json.natural_person { json.partial! 'iam/natural_person', identity: identity, natural_person: identity.physical_person } +end + +json.addresses identity.physical_address do |a| + raise 'Too many regions' if a.region.count > 1 + + json.type a.type.value.underscore.remove('_address').tap { |t| t.replace('resident') if t == 'street' } + json.inline a.address_line + + json.country { json.partial! 'iam/enumeration', value: a.country } + json.region a.region.first + json.district { json.partial! 'iam/enumeration', value: a.county } + json.municipality { json.partial! 'iam/enumeration', value: a.municipality } + json.part a.district + json.street a.street_name + json.building_number a.building_number + json.registration_number a.property_registration_number + json.unit a.unit + + # TODO + # json.address_point do + # end + + json.building_index a.building_index + + json.delivery_address do + if a.delivery_address + json.(a.delivery_address, :postal_code, :post_office_box) + + json.recipient do + if a.delivery_address.recipient + if a.delivery_address.recipient.organization_unit || a.delivery_address.recipient.corporate_body_name + json.corporate_body do + json.organization_unit a.delivery_address.recipient.organization_unit + json.partial! 'iam/corporate_body_name', corporate_body: a.delivery_address.recipient + end + end + + if a.delivery_address.recipient.person_name + json.natural_person do + json.partial! 'iam/natural_person_name', natural_person: a.delivery_address.recipient.person_name + end + end + + json.note a.delivery_address.recipient.additional_text + else + json.nil! + end + end + else + json.nil! + end + end + + json.ra_entry a.address_register_entry + json.specified a.is_specified +end + +json.emails identity.internet_address do |e| + json.(e, :address, :dsig_key_info) +end + +json.phones identity.telephone_address do |p| + json.type { json.partial! 'iam/enumeration', value: p.telephone_type } + json.number p.telephone_number.formatted_number&.value + json.(p.telephone_number, :international_country_code, :national_number, :area_city_code, :subscriber_number, :extension) +end diff --git a/app/views/iam/_natural_person.json.jbuilder b/app/views/iam/_natural_person.json.jbuilder new file mode 100644 index 00000000..76c33536 --- /dev/null +++ b/app/views/iam/_natural_person.json.jbuilder @@ -0,0 +1,49 @@ +# TODO +# natural_person.pco + +json.type { json.partial! 'iam/enumeration', value: natural_person.identity_type_detail } + +json.partial! 'iam/natural_person_name', natural_person: natural_person.person_name +json.alternative_names natural_person.alternative_name + +json.gender { json.partial! 'iam/enumeration', value: natural_person.sex } + +json.marital_status natural_person.marital_status +json.vital_status { json.partial! 'iam/enumeration', value: natural_person.death&.status } + +json.nationality { json.partial! 'iam/enumeration', value: natural_person.nationality } +json.occupation { json.partial! 'iam/enumeration', value: natural_person.occupation } + +# TODO +# json.bank_connections natural_person.bank_connection do |c| +# end + +# TODO +# json.related_persons natural_person.related_person do |c| +# end + +json.birth do + if natural_person.birth + json.date natural_person.birth.date_of_birth.to_s.in_time_zone.to_date + json.country { json.partial! 'iam/enumeration', value: natural_person.birth.country } + json.district { json.partial! 'iam/enumeration', value: natural_person.birth.county } + json.municipality { json.partial! 'iam/enumeration', value: natural_person.birth.municipality } + json.part natural_person.birth.district + else + json.nil! + end +end + +json.death do + if natural_person.death + json.date natural_person.death.date_of_death.to_s.in_time_zone.to_date + json.country { json.partial! 'iam/enumeration', value: natural_person.death.country } + json.district { json.partial! 'iam/enumeration', value: natural_person.death.county } + json.municipality { json.partial! 'iam/enumeration', value: natural_person.death.municipality } + json.part natural_person.death.district + else + json.nil! + end +end + +json.updated_on natural_person.death&.date_of_status_change.to_s.to_date diff --git a/app/views/iam/_natural_person_name.json.jbuilder b/app/views/iam/_natural_person_name.json.jbuilder new file mode 100644 index 00000000..7adaa441 --- /dev/null +++ b/app/views/iam/_natural_person_name.json.jbuilder @@ -0,0 +1,19 @@ +json.name natural_person.formatted_name +json.given_names natural_person.given_name +json.preferred_given_name natural_person.preferred_given_name + +json.given_family_names natural_person.given_family_name do |n| + json.primary n.is_primary + json.(n, :prefix, :value) +end + +json.family_names natural_person.family_name do |n| + json.primary n.is_primary + json.(n, :prefix, :value) +end + +json.legal_name natural_person.legal_name +json.other_name natural_person.other_name + +json.prefixes natural_person.affix.select { |a| a.position.to_s == 'Prefix' }, partial: 'iam/affix' +json.suffixes natural_person.affix.select { |a| a.position.to_s == 'Postfix' }, partial: 'iam/affix' diff --git a/app/views/iam/identities/search.json.jbuilder b/app/views/iam/identities/search.json.jbuilder index 77a79810..300fe5d5 100644 --- a/app/views/iam/identities/search.json.jbuilder +++ b/app/views/iam/identities/search.json.jbuilder @@ -1 +1 @@ -json.array! @identities, partial: 'identity', as: :identity +json.array! @identities, partial: 'iam/identity', as: :identity diff --git a/app/views/iam/identities/show.json.jbuilder b/app/views/iam/identities/show.json.jbuilder index 0f1cd5ea..7cc04ffe 100644 --- a/app/views/iam/identities/show.json.jbuilder +++ b/app/views/iam/identities/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'identity', identity: @identity +json.partial! 'iam/identity', identity: @identity diff --git a/config/routes.rb b/config/routes.rb index c326fb90..8bafd96b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,7 +42,8 @@ if UpvsEnvironment.sso_support? namespace :upvs do - get :assertion, path: 'sso/assertion' + get :assertion + get :identity end end end diff --git a/public/openapi.yaml b/public/openapi.yaml index 7317edbb..2f679a5c 100644 --- a/public/openapi.yaml +++ b/public/openapi.yaml @@ -119,12 +119,12 @@ paths: security: - 'API + OBO Token': [] - /api/upvs/sso/assertion: + /api/upvs/assertion: get: tags: [Informácie o prihlásenom používateľovi] - summary: Vráti informácie o prihlásenom používateľovi + summary: Vráti bezpečnostné informácie prihláseného používateľa description: | - Vráti informácie o prihlásenom používateľovi. + Vráti bezpečnostné informácie prihláseného používateľa. parameters: - name: Accept in: header @@ -135,7 +135,7 @@ paths: example: application/samlassertion+xml responses: 200: - description: Úspešne vrátené informácie o prihlásenom používateľovi. + description: Úspešne vrátené bezpečnostné informácie prihláseného používateľa. content: application/samlassertion+xml: schema: @@ -149,6 +149,24 @@ paths: security: - 'API + OBO Token': [] + /api/upvs/identity: + get: + tags: [Informácie o prihlásenom používateľovi] + summary: Vráti identitu prihláseného používateľa + description: | + Vráti identitu prihláseného používateľa. + + Pozor, volanie je dostupné len pre OVM. + responses: + 200: + description: Úspešne vrátená identita prihláseného používateľa. + content: + application/json: + schema: + $ref: '#/components/schemas/UpvsNaturalPerson' + security: + - 'API + OBO Token': [] + /api/sktalk/receive: post: tags: [Zasielanie podaní] @@ -198,7 +216,7 @@ paths: Nakoľko ide o dve operácie, ktoré nie je možné transakčne spojiť, možu nastať prípady keď odoslanie prebehne úspešne ale uloženie zlyhá. - *Volanie neuloží správu medzi odoslané ak výsledok odoslania nemá hodnotu `0` alebo ak prišlo k vypršaniu času požiadavky na ÚPVS.* + Volanie neuloží správu medzi odoslané ak výsledok odoslania nemá hodnotu `0` alebo ak prišlo k vypršaniu času požiadavky na ÚPVS requestBody: required: true content: @@ -527,7 +545,7 @@ paths: description: | Presunie správu do iného priečinka v schránke. - *Volanie vráti neúspešnú odpoveď v prípade keď správa alebo priečinok neexistuje.* + Volanie vráti neúspešnú odpoveď v prípade keď správa alebo priečinok neexistuje. parameters: - $ref: '#/components/parameters/EdeskMessageId' requestBody: @@ -554,7 +572,7 @@ paths: description: | Vymaže správu zo schránky. - *Volanie vráti úspešnú odpoveď aj v prípade keď správa už bola vymazaná alebo neexistuje.* + Volanie vráti úspešnú odpoveď aj v prípade keď správa už bola vymazaná alebo neexistuje. parameters: - $ref: '#/components/parameters/EdeskMessageId' responses: @@ -756,7 +774,7 @@ paths: description: | Odošle požiadavku na vloženie nového záznamu do úložiska. Do schránky príde odpoveď s SKTalk triedou `MDURZ_INGEST_REPLY_04`. Voči zaslanej požiadavke je možné párovanie cez poskytnutý `request_id`. - *Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe.* + Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe. parameters: - name: request_id in: query @@ -870,7 +888,7 @@ paths: description: | Odošle požiadavku na zmenu popisných údajov záznamu v úložisku. Do schránky príde odpoveď s SKTalk triedou `MDURZ_SET_METADATA_REPLY_04`. Voči zaslanej požiadavke je možné párovanie cez poskytnutý `request_id`. - *Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe.* + Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe. parameters: - name: id in: path @@ -974,7 +992,7 @@ paths: description: | Odošle požiadavku na vyradenie záznamu z úložiska. Do schránky príde odpoveď s SKTalk triedou `MDURZ_CANCEL_PRESERVATION_REPLY_04` obsahujúca uložený záznam. Voči zaslanej požiadavke je možné párovanie cez poskytnutý `request_id`. - *Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe.* + Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe. parameters: - name: id in: path @@ -1060,7 +1078,7 @@ paths: MDURZ zasiela notifikačné správy o blížiacej sa končicej lehote s SKTalk triedou `MDURZ_EXPIRATION_WARNING_04` a notifikačné správy o expirovanom zázname s SKTalk triedou `MDURZ_EXPIRATION_NOTIFICATION_04`. - *Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe.* + Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe. parameters: - name: id in: path @@ -1148,7 +1166,7 @@ paths: description: | Odošle požiadavku na získanie záznamu z úložiska. Do schránky príde odpoveď s SKTalk triedou `MDURZ_DISSEMINATE_REPLY_04` obsahujúca uložený záznam. Voči zaslanej požiadavke je možné párovanie cez poskytnutý `request_id`. - *Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe.* + Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe. parameters: - name: id in: path @@ -1227,12 +1245,14 @@ paths: /api/cuet/documents/publish_document: post: - tags: [Centrálna úradná tabuľa (len OVM)] + tags: [Centrálna úradná tabuľa (dostupné len pre OVM)] summary: Zverejnení dokument description: | Odošle požiadavku na zverejnenie dokumentu. Do schránky príde odpoveď s SKTalk triedou `CUET_NOTIFY_ENTERING`. Voči zaslanej požiadavke je možné párovanie cez poskytnutý `request_id`. - *Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe.* + Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe. + + Pozor, volanie je dostupné len pre OVM. parameters: - name: request_id in: query @@ -1353,12 +1373,14 @@ paths: /api/cuet/documents/deliver_by_publishing: post: - tags: [Centrálna úradná tabuľa (len OVM)] + tags: [Centrálna úradná tabuľa (dostupné len pre OVM)] summary: Doručí dokument zverejnením description: | Odošle požiadavku na doručenie dokumentu zverejnením. Do schránky príde odpoveď s SKTalk triedou `CUET_NOTIFY_DELIVERY`. Voči zaslanej požiadavke je možné párovanie cez poskytnutý `request_id`. - *Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe.* + Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe. + + Pozor, volanie je dostupné len pre OVM. parameters: - name: request_id in: query @@ -1479,12 +1501,14 @@ paths: /api/cuet/documents/{id}: patch: - tags: [Centrálna úradná tabuľa (len OVM)] + tags: [Centrálna úradná tabuľa (dostupné len pre OVM)] summary: Zmení dobu zverejnenia dokumentu description: | Odošle požiadavku na zmenu doby zverejnenia dokumentu. Do schránky príde odpoveď s SKTalk triedou `CUET_NOTIFY_REVOCATION`. Voči zaslanej požiadavke je možné párovanie cez poskytnutý `request_id`. - *Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe.* + Volanie automaticky validuje, či je interne používaná správna verzia formulára. Nie je potrebné kontrolovať validitu formulára externe. + + Pozor, volanie je dostupné len pre OVM. parameters: - name: id in: path @@ -1568,10 +1592,12 @@ paths: /api/iam/identities/{id}: get: - tags: [Vyhľadávanie identít (len OVM)] + tags: [Vyhľadávanie identít (dostupné len pre OVM)] summary: Vráti identitu description: | Vráti identitu. + + Pozor, volanie je dostupné len pre OVM. parameters: - name: id in: path @@ -1599,14 +1625,15 @@ paths: $ref: '#/components/schemas/UpvsNaturalPerson/example' security: - 'API Token': [] - - 'API + OBO Token': [] /api/iam/identities/search: post: - tags: [Vyhľadávanie identít (len OVM)] + tags: [Vyhľadávanie identít (dostupné len pre OVM)] summary: Vyhľadá identity podľa kritérií description: | Vyhľadá identity podľa kritérií. + + Pozor, volanie je dostupné len pre OVM. requestBody: required: true content: @@ -1793,7 +1820,6 @@ paths: - $ref: '#/components/schemas/UpvsNaturalPerson' security: - 'API Token': [] - - 'API + OBO Token': [] /api/usr: post: diff --git a/spec/requests/api/iam_spec.rb b/spec/requests/api/iam_spec.rb index 5b59d46c..3da75c9c 100644 --- a/spec/requests/api/iam_spec.rb +++ b/spec/requests/api/iam_spec.rb @@ -16,7 +16,7 @@ def set_upvs_expectations it 'returns identity' do set_upvs_expectations - get '/api/iam/identities/6d9dc77b-70ed-432f-abaa-5de8753c967c', headers: { 'Authorization' => 'Bearer ' + token } + get '/api/iam/identities/6d9dc77b-70ed-432f-abaa-5de8753c967c', headers: headers expect(response.status).to eq(200) expect(response.object).to eq(JSON.parse(file_fixture('api/iam/identity.json').read, symbolize_names: true)) diff --git a/spec/requests/api/sktalk_spec.rb b/spec/requests/api/sktalk_spec.rb index 91022e9c..8daeea4a 100644 --- a/spec/requests/api/sktalk_spec.rb +++ b/spec/requests/api/sktalk_spec.rb @@ -39,8 +39,8 @@ def set_upvs_expectations expect(response.object).to eq(receive_result: 3100119) end - include_examples 'API request media types', post: '/api/sktalk/receive', allow_sub: true, allow_obo_token: true, accept: 'application/json' - include_examples 'API request authentication', post: '/api/sktalk/receive', allow_sub: true, allow_obo_token: true, require_obo_token_scope: 'sktalk/receive' + include_examples 'API request media types', post: '/api/sktalk/receive', accept: 'application/json' + include_examples 'API request authentication', post: '/api/sktalk/receive', allow_sub: true, allow_obo_token: true it 'responds with 400 if request does not contain message' do post '/api/sktalk/receive', headers: headers, params: params.except(:message), as: :json @@ -83,7 +83,7 @@ def set_upvs_expectations pending 'responds with 429 if request rate limit exceeds' include_examples 'URP request failure', post: '/api/sktalk/receive', receive: true - include_examples 'UPVS proxy initialization', post: '/api/sktalk/receive', allow_sub: true, allow_obo_token: true, require_obo_token_scope: 'sktalk/receive' + include_examples 'UPVS proxy initialization', post: '/api/sktalk/receive', allow_sub: true, allow_obo_token: true end describe 'POST /api/sktalk/receive_and_save_to_outbox' do @@ -129,8 +129,8 @@ def set_upvs_expectations expect(response.object).to eq(receive_result: 0, receive_timeout: false, save_to_outbox_result: 3100119, save_to_outbox_timeout: false) end - include_examples 'API request media types', post: '/api/sktalk/receive_and_save_to_outbox', allow_sub: true, allow_obo_token: true, accept: 'application/json' - include_examples 'API request authentication', post: '/api/sktalk/receive_and_save_to_outbox', allow_sub: true, allow_obo_token: true, require_obo_token_scope: 'sktalk/receive_and_save_to_outbox' + include_examples 'API request media types', post: '/api/sktalk/receive_and_save_to_outbox', accept: 'application/json' + include_examples 'API request authentication', post: '/api/sktalk/receive_and_save_to_outbox', allow_sub: true, allow_obo_token: true it 'responds with 400 if request does not contain message' do post '/api/sktalk/receive_and_save_to_outbox', headers: headers, params: params.except(:message), as: :json @@ -173,7 +173,7 @@ def set_upvs_expectations pending 'responds with 429 if request rate limit exceeds' include_examples 'URP request failure', post: '/api/sktalk/receive_and_save_to_outbox', receive: true, save_to_outbox: true - include_examples 'UPVS proxy initialization', post: '/api/sktalk/receive_and_save_to_outbox', allow_sub: true, allow_obo_token: true, require_obo_token_scope: 'sktalk/receive_and_save_to_outbox' + include_examples 'UPVS proxy initialization', post: '/api/sktalk/receive_and_save_to_outbox', allow_sub: true, allow_obo_token: true end describe 'POST /api/sktalk/save_to_outbox' do @@ -207,8 +207,8 @@ def set_upvs_expectations expect(response.object).to eq(save_to_outbox_result: 3100119) end - include_examples 'API request media types', post: '/api/sktalk/save_to_outbox', allow_sub: true, allow_obo_token: true, accept: 'application/json' - include_examples 'API request authentication', post: '/api/sktalk/save_to_outbox', allow_sub: true, allow_obo_token: true, require_obo_token_scope: 'sktalk/save_to_outbox' + include_examples 'API request media types', post: '/api/sktalk/save_to_outbox', accept: 'application/json' + include_examples 'API request authentication', post: '/api/sktalk/save_to_outbox', allow_sub: true, allow_obo_token: true it 'responds with 400 if request does not contain message' do post '/api/sktalk/save_to_outbox', headers: headers, params: params.except(:message), as: :json @@ -251,6 +251,6 @@ def set_upvs_expectations pending 'responds with 429 if request rate limit exceeds' include_examples 'URP request failure', post: '/api/sktalk/save_to_outbox', save_to_outbox: true - include_examples 'UPVS proxy initialization', post: '/api/sktalk/save_to_outbox', allow_sub: true, allow_obo_token: true, require_obo_token_scope: 'sktalk/save_to_outbox' + include_examples 'UPVS proxy initialization', post: '/api/sktalk/save_to_outbox', allow_sub: true, allow_obo_token: true end end diff --git a/spec/requests/api/upvs_spec.rb b/spec/requests/api/upvs_spec.rb index 47c3ee03..aac526fa 100644 --- a/spec/requests/api/upvs_spec.rb +++ b/spec/requests/api/upvs_spec.rb @@ -5,9 +5,10 @@ allow_api_token_with_obo_token! skip_upvs_subject_verification! - let(:token) { api_token_with_obo_token } + let(:token) { api_token_with_obo_token(scopes: ['upvs/assertion', 'upvs/identity']) } + let(:upvs) { upvs_proxy_double } - describe 'GET /api/upvs/sso/assertion' do + describe 'GET /api/upvs/assertion' do let(:headers) do { 'Authorization' => 'Bearer ' + token, @@ -18,21 +19,88 @@ let(:assertion) { file_fixture('oam/sso_response_success_assertion.xml').read.strip } it 'returns SAML assertion' do - get '/api/upvs/sso/assertion', headers: headers + get '/api/upvs/assertion', headers: headers expect(response.status).to eq(200) expect(response.body).to eq(assertion) end - include_examples 'API request media types', get: '/api/upvs/sso/assertion', accept: 'application/samlassertion+xml' - include_examples 'API request authentication', get: '/api/upvs/sso/assertion', allow_obo_token: true + include_examples 'API request media types', get: '/api/upvs/assertion', accept: 'application/samlassertion+xml' + include_examples 'API request authentication', get: '/api/upvs/assertion', allow_obo_token: true + end + + describe 'GET /api/upvs/identity' do + def set_upvs_expectations + # TODO test against request template here not just class -> use custom matcher which does UpvsObjects.to_structure(actual) == UpvsObjects.to_structure(xxx_request('xxx/xxx_request.xml')) + expect(upvs.iam).to receive(:get_identity).with(kind_of(sk.gov.schemas.identity.service._1.GetIdentityRequest)).and_return(iam_response('iam/get_identity_response.xml')) + end + + it 'returns identity' do + set_upvs_expectations + + get '/api/upvs/identity', headers: headers + + expect(response.status).to eq(200) + expect(response.object).to eq(JSON.parse(file_fixture('api/iam/identity.json').read, symbolize_names: true)) + end + + include_examples 'API request media types', get: '/api/upvs/identity', accept: 'application/json' + include_examples 'API request authentication', get: '/api/upvs/identity', allow_obo_token: true + + it 'responds with 400 if IAM raises IAM fault' do + expect(upvs.iam).to receive(:get_identity).with(kind_of(sk.gov.schemas.identity.service._1.GetIdentityRequest)).and_raise(iam_get_identity_fault('iam/get_identity/undefined_fault.xml')) + + get '/api/upvs/identity', headers: headers + + expect(response.status).to eq(400) + expect(response.object).to eq(message: 'Invalid identity identifier', fault: { code: '00000000', reason: 'Nedefinovaná chyba!' }) + end + + it 'responds with 408 if IAM raises timeout error' do + expect(upvs.iam).to receive(:get_identity).and_raise(soap_timeout_exception) + + get '/api/upvs/identity', headers: headers + + expect(response.status).to eq(408) + expect(response.object).to eq(message: 'Operation timeout exceeded') + end + + pending 'responds with 429 if request rate limit exceeds' + + it 'responds with 500 if IAM raises internal error' do + expect(upvs.iam).to receive(:get_identity).and_raise + + get '/api/upvs/identity', headers: headers + + expect(response.status).to eq(500) + expect(response.object).to eq(message: 'Unknown error') + end + + it 'responds with 503 if IAM raises SOAP fault' do + expect(upvs.iam).to receive(:get_identity).and_raise(soap_fault_exception) + + get '/api/upvs/identity', headers: headers + + expect(response.status).to eq(503) + expect(response.object).to eq(message: 'Unknown failure') + end + + include_examples 'UPVS proxy initialization', get: '/api/upvs/identity', allow_obo_token: true end end context 'without UPVS SSO support', unless: sso_support? do - describe 'GET /api/upvs/sso/assertion' do + describe 'GET /api/upvs/assertion' do + it 'responds with 404' do + get '/api/upvs/assertion' + + expect(response.status).to eq(404) + end + end + + describe 'GET /api/upvs/identity' do it 'responds with 404' do - get '/api/upvs/sso/assertion' + get '/api/upvs/identity' expect(response.status).to eq(404) end diff --git a/spec/requests/sso/upvs_spec.rb b/spec/requests/sso/upvs_spec.rb index 5e9a6291..948e2385 100644 --- a/spec/requests/sso/upvs_spec.rb +++ b/spec/requests/sso/upvs_spec.rb @@ -229,7 +229,7 @@ def mock_idp_response(response) end it 'responds with 400 if request does not contain callback URL' do - get '/auth/saml/logout', headers: { 'Authorization' => 'Bearer ' + token } + get '/auth/saml/logout', headers: { 'Authorization' => 'Bearer ' + token }, params: {} expect(response.status).to eq(400) expect(response.object).to eq(message: 'No callback') diff --git a/spec/services/obo_token_authenticator_spec.rb b/spec/services/obo_token_authenticator_spec.rb index 0df2c40e..caae7db4 100644 --- a/spec/services/obo_token_authenticator_spec.rb +++ b/spec/services/obo_token_authenticator_spec.rb @@ -150,7 +150,7 @@ let(:token) { subject.generate_token(response) } it 'returns true' do - expect(subject.invalidate_token(token)).to be true + expect(subject.invalidate_token(token)).to eq(true) end it 'verifies token' do diff --git a/spec/support/api_requests.rb b/spec/support/api_requests.rb index f597633c..1e88ea9b 100644 --- a/spec/support/api_requests.rb +++ b/spec/support/api_requests.rb @@ -1,10 +1,12 @@ module ApiRequests def setup_api_requests(method_to_path) + raise ArgumentError if method_to_path.size != 1 + # TODO consider forcing definitions of method + path -> repeating it in examples is prone to typos # remove **method_to_path from shared examples below, then add: # raise 'No method' unless method_defined?(:method) # raise 'No path' unless method_defined?(:path) - # TODO consider a definition for allow_plain, allow_sub, allow_obo_token and require_obo_token_scope -> repeating it in examples is prone to typos + # TODO consider a definition for allow_plain, allow_sub, allow_obo_token -> repeating it in examples is prone to typos # raise 'No authenticator options' unless method_defined?(:authenticator_options) # TODO then consider using #send_request in each example to eliminate method/path -> repeating it in examples is prone to typos # def send_request(*args, **options) @@ -157,7 +159,7 @@ def skip_upvs_subject_verification! end if expect_response_body end -shared_examples 'API request authentication' do |allow_plain: false, allow_sub: false, allow_obo_token: false, require_obo_token_scope: nil, **method_to_path| +shared_examples 'API request authentication' do |allow_plain: false, allow_sub: false, allow_obo_token: false, **method_to_path| raise ArgumentError unless allow_plain || allow_sub || allow_obo_token method, path = setup_api_requests(method_to_path) @@ -205,7 +207,7 @@ def skip_upvs_subject_verification! it 'allows authentication via tokens with CTY header + OBO token', if: sso_support? do set_upvs_expectations - send method, path, headers: headers.merge('Authorization' => 'Bearer ' + api_token_with_obo_token(scopes: [require_obo_token_scope])), params: params, as: format + send method, path, headers: headers.merge('Authorization' => 'Bearer ' + api_token_with_obo_token(scopes: [obo_token_scope(method, path)])), params: params, as: format expect(response).to be_successful end if allow_obo_token @@ -263,7 +265,7 @@ def skip_upvs_subject_verification! end if allow_sub it 'responds with 401 if authenticating via token with CTY header + OBO token', if: sso_support? do - send method, path, headers: headers.merge('Authorization' => 'Bearer ' + api_token_with_obo_token(scopes: [require_obo_token_scope])), params: params, as: format + send method, path, headers: headers.merge('Authorization' => 'Bearer ' + api_token_with_obo_token(scopes: [obo_token_scope(method, path)])), params: params, as: format expect(response.status).to eq(401) expect(response.object).to eq(message: 'Bad credentials') @@ -274,7 +276,7 @@ def skip_upvs_subject_verification! expect(response.status).to eq(401) expect(response.object).to eq(message: 'Bad credentials') - end if allow_obo_token && require_obo_token_scope + end if allow_obo_token it 'responds with 401 if authenticating via token with CTY header + OBO token', unless: sso_support? do # OBO tokens can not be generated nor verified therefore authentication will never succeed @@ -282,7 +284,7 @@ def skip_upvs_subject_verification! end end -shared_examples 'UPVS proxy initialization' do |allow_plain: false, allow_sub: false, allow_obo_token: false, require_obo_token_scope: nil, **method_to_path| +shared_examples 'UPVS proxy initialization' do |allow_plain: false, allow_sub: false, allow_obo_token: false, **method_to_path| raise ArgumentError unless allow_plain || allow_sub || allow_obo_token method, path = setup_api_requests(method_to_path) @@ -338,7 +340,7 @@ def skip_upvs_subject_verification! 2.times do set_upvs_expectations - send method, path, headers: headers.merge('Authorization' => 'Bearer ' + api_token_with_obo_token(scopes: [require_obo_token_scope])), params: params, as: format + send method, path, headers: headers.merge('Authorization' => 'Bearer ' + api_token_with_obo_token(scopes: [obo_token_scope(method, path)])), params: params, as: format expect(response).to be_successful end @@ -349,7 +351,7 @@ def skip_upvs_subject_verification! expect(UpvsProxy).not_to receive(:new) 2.times do - send method, path, headers: headers.merge('Authorization' => 'Bearer ' + api_token_with_obo_token(scopes: [require_obo_token_scope])), params: params, as: format + send method, path, headers: headers.merge('Authorization' => 'Bearer ' + api_token_with_obo_token(scopes: [obo_token_scope(method, path)])), params: params, as: format expect(response).not_to be_successful end diff --git a/spec/support/authenticity_tokens.rb b/spec/support/authenticity_tokens.rb index 693ed14f..a3053048 100644 --- a/spec/support/authenticity_tokens.rb +++ b/spec/support/authenticity_tokens.rb @@ -38,6 +38,10 @@ def obo_token_from_response(response, scopes: []) response = OneLogin::RubySaml::Response.new(response) unless response.is_a?(OneLogin::RubySaml::Response) Environment.obo_token_authenticator.generate_token(response, scopes: scopes) end + + def obo_token_scope(method, path) + Rails.application.routes.recognize_path(path, method: method).slice(:controller, :action).values.join('/') + end end RSpec.configure do |config|