Skip to content

Commit

Permalink
Release v3.0.3
Browse files Browse the repository at this point in the history
  • Loading branch information
luciajanikova committed Apr 5, 2022
1 parent ca3865d commit 80069b7
Show file tree
Hide file tree
Showing 14 changed files with 272 additions and 32 deletions.
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
**Inštalačná príručka popisuje komponent verzie [3.0.2](https://github.com/slovensko-digital/slovensko-sk-api/releases/tag/v3.0.2), 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 [3.0.3](https://github.com/slovensko-digital/slovensko-sk-api/releases/tag/v3.0.3), 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

Expand Down
24 changes: 24 additions & 0 deletions app/controllers/administration/certificates_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class Administration::CertificatesController < ApiController
before_action { authenticate(allow_plain: true) }

before_action(only: :create) { head :conflict if UpvsEnvironment.subject?(params[:id]) }
before_action(only: [:show, :destroy]) { head :not_found unless UpvsEnvironment.subject?(params[:id]) }

def create
UpvsEnvironment.create_subject(params[:id], **params.permit(:cin).to_options)

head :created
end

def show
subject = UpvsEnvironment.subject(params[:id])

render json: subject
end

def destroy
UpvsEnvironment.delete_subject(params[:id])

head :no_content
end
end
2 changes: 1 addition & 1 deletion app/controllers/health_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def index
status = :ok
health = {
description: 'slovensko.sk API',
version: '3.0.2',
version: '3.0.3',
status: 'pass',
checks: {
'environment:variables' => environment_variables,
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/iam/identities_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ def show
def search
query = params.permit(
:match, :page, :per_page,
:ids, :uris, :en, :email, :phone,
:en, :email, :phone,
ids: [],
uris: [],
address: [:type, :country, :district, :municipality, :street, :building_number, :registration_number],
corporate_body: [:cin, :tin, :name],
natural_person: [:given_name, :family_name, :date_of_birth, :place_of_birth],
Expand Down
7 changes: 7 additions & 0 deletions app/controllers/sktalk_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class SktalkController < ApiController
rescue_from(SktalkReceiver::ReceiveMessageFormatError) { render_bad_request(:invalid, :message) }
rescue_from(SktalkReceiver::ReceiveAsSaveToFolderError) { render_unprocessable_entity(:received_as_being_saved_to_folder) }

PREPARE_FOR_LATER_RECEIVE_SCOPES = %w[sktalk/receive sktalk/receive_and_save_to_outbox sktalk/save_to_outbox]

def receive
render json: { receive_result: sktalk_receiver(upvs_identity).receive(params[:message]) }
end
Expand All @@ -26,6 +28,11 @@ def save_to_outbox
# allow sending invalid sktalk to cache token for SSO
def prepare_for_later_receive(message_builder: SktalkMessageBuilder)
message = message_builder.new(class: 'EGOV_APPLICATION', posp_id: 'App.GeneralAgenda', posp_version: '1.9')

sktalk_receiver(upvs_identity).receive(message.to_xml)

long_lasting_obo_token = Environment.api_token_authenticator.generate_long_lasting_token(authenticity_token, PREPARE_FOR_LATER_RECEIVE_SCOPES)

render status: :ok, json: {"token": long_lasting_obo_token}
end
end
13 changes: 13 additions & 0 deletions app/services/api_token_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ def verify_token(token, allow_plain: false, allow_sub: false, allow_obo_token: f
[sub, obo]
end

def generate_long_lasting_token(token, scopes)
options = {
algorithm: 'RS256',
verify_expiration: false,
verify_not_before: false,
verify_jti: -> (jti) { jti =~ JTI_PATTERN },
}

payload, header = JWT.decode(token, @public_key, true, options)

@obo_token_authenticator.generate_long_lasting_token(payload['obo'], scopes)
end

private

def obo_token_support?
Expand Down
63 changes: 42 additions & 21 deletions app/services/obo_token_authenticator.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# See https://tools.ietf.org/html/rfc7519

class OboTokenAuthenticator
DECODE_OPTIONS = {
algorithm: 'RS256',
verify_expiration: false,
verify_not_before: false,
verify_iat: false,
verify_jti: true,
}

def initialize(assertion_store:, key_pair:, proxy_subject:)
@assertion_store = assertion_store
@key_pair = key_pair
Expand All @@ -26,22 +34,23 @@ def generate_token(response, scopes: [])

ass = assertion_to_s(assertion)

loop do
jti = SecureRandom.uuid
payload = { sub: sub, exp: exp, nbf: nbf, iat: iat, name: name, scopes: scopes }

payload = { sub: sub, exp: exp, nbf: nbf, iat: iat, jti: jti, name: name, scopes: scopes }
exp_in = exp - Time.now.to_f
save_to_assertion_store(ass, payload)
end

raise ArgumentError, :exp if exp_in <= 0
def generate_long_lasting_token(token, scopes)
payload, header = JWT.decode(token, @key_pair.public_key, true, DECODE_OPTIONS)

next unless @assertion_store.write(jti, ass, expires_in: exp_in, unless_exist: true)
# TODO parse STS token expiration from assertion
token_expiration = Time.now.to_i + 120.minutes.to_i

begin
return JWT.encode(payload, @key_pair, 'RS256')
rescue => error
@assertion_store.delete(jti) and raise(error)
end
end
payload['exp'] = token_expiration
payload['scopes'] = scopes

assertion = @assertion_store.read(payload['jti'])

save_to_assertion_store(assertion, payload.symbolize_keys)
end

def invalidate_token(token)
Expand All @@ -52,15 +61,7 @@ def invalidate_token(token)
end

def verify_token(token, scope: nil)
options = {
algorithm: 'RS256',
verify_expiration: false,
verify_not_before: false,
verify_iat: false,
verify_jti: true,
}

payload, header = JWT.decode(token, @key_pair.public_key, true, options)
payload, header = JWT.decode(token, @key_pair.public_key, true, DECODE_OPTIONS)
exp, nbf, iat, jti = payload['exp'], payload['nbf'], payload['iat'], payload['jti']

raise JWT::InvalidPayload, :exp unless exp.is_a?(Integer)
Expand Down Expand Up @@ -88,6 +89,26 @@ def verify_token(token, scope: nil)

private

def save_to_assertion_store(assertion, payload)
loop do
jti = SecureRandom.uuid

payload[:jti] = jti

exp_in = payload[:exp] - Time.now.to_f

raise ArgumentError, :exp if exp_in <= 0

next unless @assertion_store.write(jti, assertion, expires_in: exp_in, unless_exist: true)

begin
return JWT.encode(payload, @key_pair, 'RS256')
rescue => error
@assertion_store.delete(jti) and raise(error)
end
end
end

def parse_assertion(response)
document = response.decrypted_document || response.document
assertion = REXML::XPath.first(document, '//saml:Assertion')
Expand Down
36 changes: 32 additions & 4 deletions public/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.0.0

info:
title: slovensko.sk API
version: 3.0.2 (Komunitná verzia) 8.0.2 (Prémium verzia)
version: 3.0.3 (Komunitná verzia) 8.0.3 (Prémium verzia)

description: |
slovensko.sk API je proxy REST API komponent k službám www.slovensko.sk (Ústredný portál verejnej správy – ÚPVS), pomocou ktorých je možné:
Expand Down Expand Up @@ -294,10 +294,37 @@ paths:
summary: Pripraví odosielanie podania aj po vypršaní WebSSO session
description: |
Pripraví odosielanie podania aj po vypršaní WebSSO session.
responses:
204:
description: Úspešné získanie tokenu z modulu IAM.
Vráti OBO (On-Behalf-Of) token použiteľný na odoslanie podaní prihláseného používateľa aj po vypršaní WebSSO session. OBO token je platný 120 minút.
responses:
200:
description: |
OBO token v JWT formáte, ktorého payload vyzerá nasledovne:
{
"sub": "rc://sk/8311577984_tisici_janko",
"exp": 1545153549,
"nbf": 1545146349,
"iat": 1545146349,
"jti": "ad8e5d2a-85ff-46b9-a13f-ac860db9acee",
"name": "Janko Tisíci",
"scopes": [
"sktalk/receive",
"sktalk/receive_and_save_to_outbox",
"sktalk/save_to_outbox"
]
}
content:
application/json:
schema:
type: object
properties:
token:
schema:
type: string
format: jwt
required:
- token
security:
- 'API + OBO Token': []

Expand Down Expand Up @@ -3295,6 +3322,7 @@ components:
edesk_remote_uri:
description: URI adresa vzdialenej eDesk schránky.
type: string
nullable: true
format: uri
example: null
edesk_cuet_delivery_enabled:
Expand Down
Empty file added security/sso/.keep
Empty file.
Empty file added security/sts/.keep
Empty file.
Empty file added security/tls/.keep
Empty file.
142 changes: 142 additions & 0 deletions spec/requests/administration_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
require 'rails_helper'

RSpec.describe 'Administration' do
allow_api_token_with_obo_token!

let(:token) { api_token }

describe 'POST /administration/certificates' do
let(:params) do
{
id: 'iscep_00956852_10236',
cin: '00956852_10236',
}
end

after(:example) { suppress(SystemCallError) { UpvsEnvironment.delete_subject('iscep_00956852_10236') }}

it 'creates certificate' do
post '/administration/certificates', headers: headers, params: params, as: :json

expect(response.status).to eq(201)
expect(response.body).to be_empty

expect(UpvsEnvironment.subject?('iscep_00956852_10236')).to eq(true)
end

context 'keystore' do
subject { Rails.root.join('security', 'sts', "iscep_00956852_10236_#{Upvs.env}.keystore") }

before(:example) { allow(Upvs).to receive(:env).and_return(ActiveSupport::StringInquirer.new('prod')) }
before(:example) { stub_const('ENV', ENV.merge('UPVS_KS_SALT' => SecureRandom.hex(20), 'UPVS_PK_SALT' => SecureRandom.hex(20))) }

it 'has JKS format' do
post '/administration/certificates', headers: headers, params: params, as: :json

expect(subject.read(4).unpack('H*').first).to eq('feedfeed')
end

it 'has two passwords' do
ks, pk = ENV.values_at('UPVS_KS_SALT', 'UPVS_PK_SALT').map { |salt| Digest::SHA1.hexdigest("#{salt}:#{params[:id]}") }

post '/administration/certificates', headers: headers, params: params, as: :json

expect(ks).not_to eq(pk)
expect(KeyStore.new(subject.to_s, ks).private_key(params[:id], pk)).to be
end
end

pending 'responds with 400 if request does not contain identifier'
pending 'responds with 400 if request contains malicious identifier' # TODO check against shell escape stuff

pending 'responds with 400 if request does not contain CIN'
pending 'responds with 400 if request contains malicious CIN' # TODO check against shell escape stuff

it 'responds with 409 if certificate already exists' do
UpvsEnvironment.create_subject('iscep_00956852_10236', cin: '00956852_10236')

post '/administration/certificates', headers: headers, params: params, as: :json

expect(response.status).to eq(409)
expect(response.body).to be_empty
end

include_examples 'API request media types', post: '/administration/certificates', accept: 'application/json', expect_response_body: false
include_examples 'API request authentication', post: '/administration/certificates', allow_plain: true
end

describe 'GET /administration/certificates/{id}' do
before(:example) { UpvsEnvironment.create_subject('iscep_00956852_10236', cin: '00956852_10236') }

after(:example) { suppress(SystemCallError) { UpvsEnvironment.delete_subject('iscep_00956852_10236') }}

it 'gets certificate' do
get '/administration/certificates/iscep_00956852_10236', headers: headers

expect(response.status).to eq(200)
expect(response.object.keys).to contain_exactly(:certificate, :fingerprint, :not_after, :subject)

expect(response.object[:certificate]).to match(/\A-{5}BEGIN CERTIFICATE-{5}\n.+\n-{5}END CERTIFICATE-{5}\n\z/m)
expect(response.object[:fingerprint]).to match(/\A[0-9a-f]{40}\z/)
expect(response.object[:not_after]).to eq(response.object[:not_after].in_time_zone.as_json)
expect(response.object[:subject]).to eq("ico-00956852_10236")
end

pending 'responds with 400 if request contains malicious identifier' # TODO check against shell escape stuff

it 'responds with 404 if certificate does not exist' do
UpvsEnvironment.delete_subject('iscep_00956852_10236')

get '/administration/certificates/iscep_00956852_10236', headers: headers

expect(response.status).to eq(404)
expect(response.body).to be_empty
end

include_examples 'API request media types', get: '/administration/certificates/iscep_00956852_10236', accept: 'application/json'
include_examples 'API request authentication', get: '/administration/certificates/iscep_00956852_10236', allow_plain: true
end

describe 'DELETE /administration/certificates/{id}' do
before(:example) { UpvsEnvironment.create_subject('iscep_00956852_10236', cin: '00956852_10236') }

after(:example) { suppress(SystemCallError) { UpvsEnvironment.delete_subject('iscep_00956852_10236') }}

it 'deletes certificate' do
delete '/administration/certificates/iscep_00956852_10236', headers: headers

expect(response.status).to eq(204)
expect(response.body).to be_empty

expect(UpvsEnvironment.subject?('iscep_00956852_10236')).to eq(false)
end

pending 'responds with 400 if request contains malicious identifier' # TODO check against shell escape stuff

it 'responds with 404 if certificate does not exist' do
UpvsEnvironment.delete_subject('iscep_00956852_10236')

delete '/administration/certificates/iscep_00956852_10236', headers: headers

expect(response.status).to eq(404)
expect(response.body).to be_empty
end

include_examples 'API request media types', delete: '/administration/certificates/iscep_00956852_10236', accept: 'application/json', expect_response_body: false
include_examples 'API request authentication', delete: '/administration/certificates/iscep_00956852_10236', allow_plain: true
end

describe 'GET /administration/eform/synchronize' do
it 'schedules synchronization' do
expect(DownloadFormTemplatesJob).to receive(:perform_later)

get '/administration/eform/synchronize', headers: headers

expect(response.status).to eq(204)
expect(response.body).to be_empty
end

include_examples 'API request media types', get: '/administration/eform/synchronize', accept: 'application/json', expect_response_body: false
include_examples 'API request authentication', get: '/administration/eform/synchronize', allow_plain: true
end
end
Loading

0 comments on commit 80069b7

Please sign in to comment.