Skip to content
This repository has been archived by the owner on Dec 6, 2022. It is now read-only.

Commit

Permalink
Adding support for attachments (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
universal authored and jcs committed Sep 10, 2018
1 parent 287ebc7 commit ce9d0c4
Show file tree
Hide file tree
Showing 13 changed files with 374 additions and 27 deletions.
56 changes: 56 additions & 0 deletions API.md
Expand Up @@ -592,6 +592,62 @@ Send an empty `DELETE` request to `$baseURL/ciphers/(cipher UUID)`:

A successful but zero-length response will be returned.

### Adding an attachment

Send a `POST` request to `$baseURL/ciphers/(cipher UUID)/attachment`

It is a multipart/form-data post, with the file under the `data`-attribute the single posted entity.


POST $baseURL/ciphers/(cipher UUID)/attachment
Content-type: application/json
Authorization: Bearer $access_token
{
"data": {
"filename": "encrypted_filename"
"tempfile": blob
}
}

The JSON response will then be the complete cipher item, but now containing an entry for the new attachment:

{
"FolderId"=>nil,
...
"Data"=> ...,
"Attachments"=>
[
{ "Id"=>"7xytytjp1hc2ijy3n5y5vbbnzcukmo8b",
"Url"=> "https://cdn.bitwarden.com/attachments/(cipher UUID)/7xytytjp1hc2ijy3n5y5vbbnzcukmo8b",
"FileName"=> "2.GOkRA8iZio1KxB+UkJpfcA==|/Mc8ACbPr9CRRQmNKPYHVg==|4BBQf8YTbPupap6qR97qMdn0NJ88GdTgDPIyBsQ46aA=",
"Size"=>"65",
"SizeName"=>"65 Bytes",
"Object"=>"attachment"
}
],
...,
"Object"=>"cipher"
}

### Deleting an attachment

Send an empty `DELETE` request to `$baseURL/ciphers/(cipher UUID)/attachment/(attachment id)`:

DELETE $baseURL/ciphers/(cipher UUID)/attachment/(attachment id)
Authorization: Bearer (access_token)

A successful but zero-length response will be returned.

### Downloading an attachment

$cdn_url using the official server is https://cdn.bitwarden.com.

Send an unauthenticated `GET` request to `$cdn_url/attachments/(cipher UUID)/(attachment id)`:

GET $cdn_url/attachments/(cipher UUID)/(attachment id)

The file will be sent as a response.

### Folders

To create a folder, `POST` to `$baseURL/folders`:
Expand Down
13 changes: 7 additions & 6 deletions Rakefile
@@ -1,14 +1,15 @@
require "rake/testtask"

# rake db:create_migration NAME=...
require "sinatra/activerecord/rake"

Rake::TestTask.new do |t|
t.pattern = "spec/*_spec.rb"
end

namespace :db do
task :load_config do
require "./lib/rubywarden.rb"
end
end

require 'rake/testtask'

Rake::TestTask.new do |t|
t.libs << "spec"
t.pattern = "spec/*_spec.rb"
end
15 changes: 15 additions & 0 deletions db/migrate/20180818095054_create_attachments.rb
@@ -0,0 +1,15 @@
class CreateAttachments < ActiveRecord::Migration[5.1]
def change
remove_column :ciphers, :attachments
create_table :attachments, id: :string, primary_key: :uuid do |t|
t.string :cipher_uuid
t.string :url
t.string :filename
t.integer :size
t.binary :file
t.timestamps
end
add_foreign_key :attachments, :ciphers, { column: :cipher_uuid, primary_key: :uuid }
add_index(:attachments, :cipher_uuid)
end
end
4 changes: 4 additions & 0 deletions lib/app.rb
Expand Up @@ -18,10 +18,12 @@
require 'sinatra/namespace'

require_relative 'helpers/request_helpers'
require_relative 'helpers/attachment_helpers'

require_relative 'routes/api'
require_relative 'routes/icons'
require_relative 'routes/identity'
require_relative 'routes/attachments'

module Rubywarden
class App < Sinatra::Base
Expand All @@ -36,6 +38,7 @@ class App < Sinatra::Base
end

helpers Rubywarden::RequestHelpers
helpers Rubywarden::AttachmentHelpers

before do
if request.content_type.to_s.match(/\Aapplication\/json(;|\z)/)
Expand All @@ -58,5 +61,6 @@ class App < Sinatra::Base
register Rubywarden::Routing::Api
register Rubywarden::Routing::Icons
register Rubywarden::Routing::Identity
register Rubywarden::Routing::Attachments
end
end
54 changes: 54 additions & 0 deletions lib/attachment.rb
@@ -0,0 +1,54 @@
#
# Copyright (c) 2017 joshua stein <jcs@jcs.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#

class Attachment < DBModel
self.table_name = "attachments"
attr_accessor :context

before_create :generate_uuid_primary_key
before_create :generate_url

belongs_to :cipher, foreign_key: :cipher_uuid, inverse_of: :attachments

def self.build_from_params(params, context)
attachment = new filename: params[:filename],
size: params[:size],
file: params[:file]
attachment.context = context
attachment
end

def to_hash
{
"Id" => self.uuid,
"Url" => self.url,
"FileName" => self.filename.to_s,
"Size" => self.size,
"SizeName" => human_file_size,
"Object" => "attachment"
}
end

private

def generate_url
self.url = context.url("/attachments/#{self.cipher_uuid}/#{self.id}")
end

def human_file_size
ActiveSupport::NumberHelper.number_to_human_size(self.size)
end
end
4 changes: 2 additions & 2 deletions lib/cipher.rb
Expand Up @@ -22,13 +22,13 @@ class Cipher < DBModel

belongs_to :user, foreign_key: :user_uuid, inverse_of: :folders
belongs_to :folder, foreign_key: :folder_uuid, inverse_of: :ciphers, optional: true
has_many :attachments, foreign_key: :cipher_uuid, dependent: :destroy

serialize :fields, JSON
serialize :login, JSON
serialize :securenote, JSON
serialize :card, JSON
serialize :identity, JSON
serialize :attachments, JSON

TYPE_LOGIN = 1
TYPE_NOTE = 2
Expand Down Expand Up @@ -89,7 +89,7 @@ def to_hash
"FolderId" => self.folder_uuid,
"Favorite" => self.favorite,
"OrganizationId" => nil,
"Attachments" => self.attachments,
"Attachments" => self.attachments.map(&:to_hash),
"OrganizationUseTotp" => false,
"Object" => "cipher",
"Name" => self.name,
Expand Down
38 changes: 38 additions & 0 deletions lib/helpers/attachment_helpers.rb
@@ -0,0 +1,38 @@
#
# Copyright (c) 2018 joshua stein <jcs@jcs.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#

module Rubywarden
module AttachmentHelpers
def retrieve_cipher(uuid: )
d = device_from_bearer
if !d
halt validation_error("invalid bearer")
end

c = nil
if uuid.blank? || !(c = Cipher.find_by_user_uuid_and_uuid(d.user_uuid, uuid))
halt validation_error("invalid cipher")
end
return c
end

def delete_attachment uuid:, attachment_uuid:
cipher = retrieve_cipher uuid: uuid
cipher.attachments.find(attachment_uuid).destroy
""
end
end
end
14 changes: 14 additions & 0 deletions lib/helpers/request_helpers.rb
Expand Up @@ -45,5 +45,19 @@ def validation_error(msg)
"Object" => "error",
}.to_json ]
end

def delete_cipher app:, uuid:
d = device_from_bearer
if !d
halt validation_error("invalid bearer")
end

c = nil
if uuid.blank? || !(c = Cipher.find_by_user_uuid_and_uuid(d.user_uuid, uuid))
halt validation_error("invalid cipher")
end
c.destroy
""
end # delete_cipher
end
end
15 changes: 1 addition & 14 deletions lib/routes/api.rb
Expand Up @@ -209,20 +209,7 @@ def self.registered(app)

# delete a cipher
delete "/ciphers/:uuid" do
d = device_from_bearer
if !d
return validation_error("invalid bearer")
end

c = nil
if params[:uuid].blank? ||
!(c = Cipher.find_by_user_uuid_and_uuid(d.user_uuid, params[:uuid]))
return validation_error("invalid cipher")
end

c.destroy

""
delete_cipher app: app, uuid: params[:uuid]
end

#
Expand Down
69 changes: 69 additions & 0 deletions lib/routes/attachments.rb
@@ -0,0 +1,69 @@
#
# Copyright (c) 2018 joshua stein <jcs@jcs.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# uses helpers/attachment_helpers
module Rubywarden
module Routing
module Attachments
def self.registered(app)
app.namespace BASE_URL do
post "/ciphers/:uuid/attachment" do
cipher = retrieve_cipher uuid: params[:uuid]

need_params(:data) do |p|
return validation_error("#{p} cannot be blank")
end

# we have to extract filename from data -> head, since data -> filename is truncated
filename = nil
if md = params[:data][:head].match(/filename=\"(\S+)\"\r\nContent-Type/)
filename = md[1]
else
return validation_error("filename cannot be blank")
end

file = params[:data][:tempfile]
attachment_params = { filename: filename,
size: file.size,
file: file.read }
attachment = cipher.attachments.build_from_params(attachment_params, self)

Attachment.transaction do
if !attachment.save
return validation_error("error saving")
end

cipher.to_hash.to_json
end
end

delete "/ciphers/:uuid/attachment/:attachment_id" do
delete_attachment uuid: params[:uuid], attachment_uuid: params[:attachment_id]
end

post "/ciphers/:uuid/attachment/:attachment_id/delete" do
delete_attachment uuid: params[:uuid], attachment_uuid: params[:attachment_id]
end
end # BASE_URL

app.get "/attachments/:uuid/:attachment_id" do
a = Attachment.find_by_uuid_and_cipher_uuid(params[:attachment_id], params[:uuid])
attachment(a.filename)
response.write(a.file)
end
end # registered app
end
end
end
1 change: 1 addition & 0 deletions lib/rubywarden.rb
Expand Up @@ -36,6 +36,7 @@
require "#{APP_ROOT}/lib/device.rb"
require "#{APP_ROOT}/lib/cipher.rb"
require "#{APP_ROOT}/lib/folder.rb"
require "#{APP_ROOT}/lib/attachment.rb"

BASE_URL ||= "/api"
IDENTITY_BASE_URL ||= "/identity"
Expand Down

0 comments on commit ce9d0c4

Please sign in to comment.