Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integrations and other update #8

Merged
merged 41 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
37e55a0
add beta version integration api description
celuchmarek Apr 11, 2024
9352f5f
update integrations api and add jwts
celuchmarek Apr 12, 2024
d6d44bf
update api
celuchmarek Apr 12, 2024
ca6194f
separate jwts for device and integration in openapi
celuchmarek Apr 12, 2024
a5c63b9
add document encryption
celuchmarek Apr 13, 2024
4968ba1
add tmp debug output
celuchmarek Apr 13, 2024
1024d41
do not enforce strict base64 key for now
celuchmarek Apr 13, 2024
c269b78
temporary add single byte to encryption key
celuchmarek Apr 13, 2024
a778771
temporary add single byte to encryption key
celuchmarek Apr 13, 2024
2fa77bb
use urlsafe base64 key
celuchmarek Apr 13, 2024
f69165d
use urlsafe base64 key
celuchmarek Apr 13, 2024
84fbc43
rm key parameter from document views
celuchmarek Apr 13, 2024
33aa7f5
add pushkey to pairing qr code
celuchmarek Apr 16, 2024
faaa95e
update qr code example path
celuchmarek Apr 16, 2024
4e7f99d
update api
celuchmarek Apr 17, 2024
4cb03f8
drop requirement for payloadMimeType and guess it based on filename i…
celuchmarek Apr 17, 2024
e3e7848
add notifications
celuchmarek Apr 17, 2024
c9c15a6
add fcm callback description
celuchmarek Apr 17, 2024
96a869d
fix type in FcmNotifier class
celuchmarek Apr 17, 2024
d847bfc
updatea api, minor fixes, allow cammelCase and snake_case parameters
celuchmarek Apr 18, 2024
e085885
expect camelCase nested params for data_to_sign_structure
celuchmarek Apr 19, 2024
e7b9f4e
add good_job, token management, and automatic document deletion
celuchmarek Apr 22, 2024
52178a5
handle SIGNATURE_NOT_IN_TACT
celuchmarek Apr 22, 2024
40ca1bb
fix error handling for unknown mime type extensions
celuchmarek Apr 23, 2024
aa6b497
minor api description update
celuchmarek Apr 24, 2024
79b74d9
add well known volume
celuchmarek Apr 26, 2024
d5c9389
add last modified headers
celuchmarek Apr 27, 2024
939eb1c
add GET signature-level
celuchmarek Apr 30, 2024
3768806
add parameters endpoint
celuchmarek Apr 30, 2024
de47fa2
handle plaintext xsd ans xslt
celuchmarek May 5, 2024
86cadd8
optimize polling on GET document and uodate swagger
celuchmarek May 5, 2024
8ce6f1d
add apple app association file
celuchmarek May 6, 2024
d683e3c
add ntfy web hook integration
celuchmarek May 15, 2024
96e7664
fix push message encryption
celuchmarek May 15, 2024
edf0a0c
fix push message encryption
celuchmarek May 15, 2024
21f83c6
fix notifications finally
celuchmarek May 15, 2024
01dda83
retun empty json on sign request
celuchmarek May 15, 2024
b5f0a1e
add redirect from qr code to app page
celuchmarek May 20, 2024
dda47a1
use custom column for signing time changes
celuchmarek Jun 10, 2024
ff15c3d
save document on sign
celuchmarek Jun 10, 2024
4521533
add self
celuchmarek Jun 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitlab/auto-deploy-values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ persistence:
accessMode: ReadWriteMany
size: 1Gi
storageClass: nfs-client
- name: autogram-server-well-known
mount:
path: /app/public/.well-known
claim:
accessMode: ReadWriteMany
size: 1Mi
storageClass: nfs-client
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ gem "rack-cors"

gem "faraday"
gem "base64"
gem "jwt"
gem "fcm"

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
Expand All @@ -51,3 +53,4 @@ group :development do
# gem "spring"
end

gem "good_job", "~> 3.28"
45 changes: 45 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ GEM
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
base64 (0.2.0)
bigdecimal (3.1.6)
bootsnap (1.18.3)
Expand All @@ -94,12 +96,36 @@ GEM
drb (2.2.0)
ruby2_keywords
erubi (1.12.0)
et-orbi (1.2.11)
tzinfo
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-net_http (3.1.0)
net-http
fcm (1.0.8)
faraday (>= 1.0.0, < 3.0)
googleauth (~> 1)
fugit (1.10.1)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
good_job (3.28.0)
activejob (>= 6.0.0)
activerecord (>= 6.0.0)
concurrent-ruby (>= 1.0.2)
fugit (>= 1.1)
railties (>= 6.0.0)
thor (>= 0.14.1)
google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a)
googleauth (1.11.0)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
io-console (0.7.2)
Expand All @@ -109,6 +135,8 @@ GEM
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jwt (2.8.1)
base64
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
Expand All @@ -119,8 +147,10 @@ GEM
net-smtp
marcel (1.0.2)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.22.2)
msgpack (1.7.2)
multi_json (1.15.0)
mutex_m (0.2.0)
net-http (0.4.1)
uri
Expand All @@ -134,6 +164,9 @@ GEM
net-smtp (0.4.0.1)
net-protocol
nio4r (2.7.0)
nokogiri (1.16.2)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.2-aarch64-linux)
racc (~> 1.4)
nokogiri (1.16.2-arm-linux)
Expand All @@ -146,11 +179,14 @@ GEM
racc (~> 1.4)
nokogiri (1.16.2-x86_64-linux)
racc (~> 1.4)
os (1.1.4)
pg (1.5.5)
psych (5.1.2)
stringio
public_suffix (5.0.5)
puma (6.4.2)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.7.3)
rack (3.0.9.1)
rack-cors (2.0.1)
Expand Down Expand Up @@ -197,6 +233,11 @@ GEM
reline (0.4.3)
io-console (~> 0.5)
ruby2_keywords (0.0.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
stringio (3.1.0)
thor (1.3.0)
timeout (0.4.1)
Expand All @@ -213,6 +254,7 @@ PLATFORMS
aarch64-linux
arm-linux
arm64-darwin
ruby
x86-linux
x86_64-darwin
x86_64-linux
Expand All @@ -223,7 +265,10 @@ DEPENDENCIES
debug (>= 1.6.2)
dotenv-rails
faraday
fcm
good_job (~> 3.28)
jbuilder
jwt
pg
puma (>= 5.0)
rack-cors
Expand Down
25 changes: 25 additions & 0 deletions app/controllers/api/v1/device_integrations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class Api::V1::DeviceIntegrationsController < ApiController
before_action :set_device

def create
integration = ApiEnvironment.integration_token_authenticator.verify_token(params.require(:integration_pairing_token), expected_aud: 'device', max_exp_in: 24.hours)
@device.integrations << integration

head 204
end

def index
@integrations = @device.integrations
end

def destroy
@device.integrations.delete(Integration.find(params.require(:id)))
head 204
end

private

def set_device
@device = ApiEnvironment.device_token_authenticator.verify_token(authenticity_token)
end
end
11 changes: 11 additions & 0 deletions app/controllers/api/v1/devices_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Api::V1::DevicesController < ApiController
def create
@device = Device.create!(device_params)
end

private

def device_params
params.permit(:display_name, :platform, :public_key, :registration_id)
end
end
123 changes: 101 additions & 22 deletions app/controllers/api/v1/documents_controller.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
class Api::V1::DocumentsController < ApplicationController
before_action :set_document, only: %i[ show datatosign sign visualization destroy ]
before_action :set_key, only: %i[ show create datatosign sign visualization ]
before_action :decrypt_document_content, only: %i[ show sign datatosign visualization destroy]
before_action :set_document, only: %i[ show datatosign sign visualization destroy parameters ]
before_action :set_key, only: %i[ create datatosign sign visualization ]
before_action :decrypt_document_content, only: %i[ sign datatosign visualization destroy]

# GET /documents/1
def show
@signers = @document.signers
modified_since = request.headers.to_h['HTTP_IF_MODIFIED_SINCE']

response.set_header('Last-Modified', @document.last_signed_at + 1.seconds)
if modified_since && Time.zone.parse(modified_since) >= @document.last_signed_at
render json: nil, status: 304
else
# expecting 1s polling on this operation
set_key
decrypt_document_content

@signers = @document.signers
end
end

# POST /documents
def create
p = document_params
@document = Document.new(parameters: p[:parameters])
@document.encrypt_file(@key, p[:document][:filename], p[:payloadMimeType], p[:document][:content])
@document.validate_parameters(p[:document][:content])
filename, mimetype = create_filename_and_mimetype(p[:document][:filename], p[:payload_mime_type])
mimetype, content, parameters = Document.convert_to_b64(mimetype, p[:document][:content], p[:parameters])

render json: @document.errors, status: :unprocessable_entity unless @document.save
@document = Document.new(parameters: parameters)
@document.encrypt_file(@key, filename, mimetype, content)
@document.validate_parameters(content, mimetype)

unless @document.save
render json: @document.errors, status: :unprocessable_entity
else
response.set_header('Last-Modified', @document.last_signed_at + 1.seconds)
end
end

# POST /documents/1/visualization
Expand All @@ -25,14 +43,14 @@ def visualization

# POST /documents/1/datatosign
def datatosign
@document.set_add_timestamp if datatosign_params[:addTimestamp]
@signing_certificate = datatosign_params.require(:signingCertificate)
@document.set_add_timestamp if datatosign_params[:add_timestamp]
@signing_certificate = datatosign_params.require(:signing_certificate)
@result = @document.datatosign(@signing_certificate)
end

# POST /documents/1/sign
def sign
@signer = @document.sign(@key, sign_params[:dataToSignStructure], sign_params[:signedData])
@signer = @document.sign(@key, sign_params[:data_to_sign_structure], sign_params[:signed_data])
render json: @document.errors, status: :unprocessable_entity unless @signer

@document = Document.find(params[:id])
Expand All @@ -44,20 +62,34 @@ def destroy
@document.destroy!
end

# GET /documents/1/parameters
def parameters
end

private
def set_document
@document = Document.find(params[:id])
end

def set_key
@key = request.headers.to_h['HTTP_X_ENCRYPTION_KEY'] || params[:encryptionKey]
key_b64 = request.headers.to_h['HTTP_X_ENCRYPTION_KEY'] || params[:encryption_key]
begin
begin
# AVM app somehow sends urlsafe base64 even if its source code doesn't seem so
@key = Base64.urlsafe_decode64(key_b64)
rescue ArgumentError
@key = Base64.strict_decode64(key_b64)
end
rescue => e
raise AvmUnauthorizedError.new("ENCRYPTION_KEY_MALFORMED", "Encryption key Base64 decryption failed.", e.message)
end

raise AvmUnauthorizedError.new("ENCRYPTION_KEY_MISSING", "Encryption key not provided.", "Encryption key must be provided either in X-Encryption-Key header or as encryptionKey query parameter.") unless @key
# TODO
# raise AvmUnauthorizedError.new("ENCRYPTION_KEY_MALFORMED", "Encryption key invalid.", "Encryption key must be a 64 character long hexadecimal string.") unless validate_key(@key)
raise AvmUnauthorizedError.new("ENCRYPTION_KEY_MALFORMED", "Encryption key invalid.", "Encryption key must be a base64 string encoding 32 bytes long key.") unless validate_key(@key)
end

def validate_key(key)
key.length == 64 and !key[/\H/]
key.length == 32
end

def decrypt_document_content
Expand All @@ -68,22 +100,69 @@ def document_params
params.require(:parameters)
d = params.require(:document)
d.require(:content)
d.require(:filename)
params.require(:payloadMimeType)
params.permit(:encryptionKey, :payloadMimeType, :key, :document => [:filename, :content], :parameters => [:level, :container])
params.permit(
:encryption_key,
:payload_mime_type,
:document => [:filename, :content],
:parameters => [
:checkPDFACompliance,
:autoLoadEform,
:level,
:container,
:containerXmlns,
:embedUsedSchemas,
:identifier,
:packaging,
:digestAlgorithm,
:en319132,
:infoCanonicalization,
:propertiesCanonicalization,
:keyInfoCanonicalization,
:schema,
:schemaIdentifier,
:transformation,
:transformationIdentifier,
:transformationLanguage,
:transformationMediaDestinationTypeDescription,
:transformationTargetEnvironment
]
)
end

def datatosign_params
params.permit(:encryptionKey, :id, :signingCertificate, :addTimestamp)
params.permit(:encryption_key, :id, :signing_certificate, :add_timestamp)
end

def sign_params
params.require(:signedData)
dts = params.require(:dataToSignStructure)
params.require(:signed_data)
dts = params.require(:data_to_sign_structure)
dts.require(:dataToSign)
dts.require(:signingTime)
dts.require(:signingCertificate)

params.permit(:encryptionKey, :id, :signedData, :returnSignedDocument, :dataToSignStructure => [:dataToSign, :signingTime, :signingCertificate])
params.permit(:encryption_key, :id, :signed_data, :return_signed_document, :data_to_sign_structure => [:dataToSign, :signingTime, :signingCertificate])
end

def create_filename_and_mimetype(filename, mimetype)
return [filename, mimetype] if filename && mimetype

Mime::Type.register("application/vnd.etsi.asic-e+zip", "asice", [], [".sce"])
Mime::Type.register("application/vnd.etsi.asic-s+zip", "asics", [], [".scs"])
Mime::Type.register("application/vnd.gov.sk.xmldatacontainer+xml", "xdcf")
Mime::Type.register("application/msword", "doc")
Mime::Type.register("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx")
Mime::Type.register("application/vnd.ms-excel", "xls")
Mime::Type.register("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx")
Mime::Type.register("application/vnd.ms-powerpoint", "ppt")
Mime::Type.register("application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx")

unless filename
filename = 'document.' + Mime::Type.lookup(mimetype).symbol.to_s
else
mimetype = Mime::Type.lookup_by_extension(File.extname(filename).downcase.gsub('.', '')).to_s
raise AvmServiceBadRequestError.new({code: "FAILED_PARSING_MIMETYPE", message: "Could not parse mimetype", details: "Could not parse mimetype from: #{filename}"}.to_json) if mimetype.empty?
end

[filename, mimetype]
end
end
18 changes: 18 additions & 0 deletions app/controllers/api/v1/integration_devices_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Api::V1::IntegrationDevicesController < ApiController
before_action :set_integration

def index
@devices = @integration.devices
end

def destroy
@integration.devices.delete(Device.find(params.require(:id)))
render :head
end

private

def set_integration
@integration = ApiEnvironment.integration_token_authenticator.verify_token(authenticity_token)
end
end
11 changes: 11 additions & 0 deletions app/controllers/api/v1/integrations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Api::V1::IntegrationsController < ApiController
def create
@integration = Integration.create!(integration_params)
end

private

def integration_params
params.permit(:display_name, :platform, :public_key, :pushkey)
end
end
Loading