diff --git a/README.md b/README.md index 0d13511..b12b9e6 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,13 @@ A group of commands that will allow you to: ## Structures ### URI List -- One URI per line. Blank lines, or lines that begin with '#', are ignored. +- One URI per line. +- White space at the beginning or end of the line is ignored. +- Blank lines, or lines that begin with '#', are ignored. ### Session List -- One request session per line. Blank lines, or lines that begin with '#', are ignored. +- One request session per line. +- Blank lines, or lines that begin with '#', are ignored. - A request may be a single URL. - A request may also specify POST or GET, HTTP headers, and parameters. - A session may contain multiple requests, with an optional login/logout specification. @@ -48,11 +51,10 @@ You provide a file of URIs, and the tool will generate the URLs needed to fetch If you want a login on each session, provide the email address and password of the desired login account. ``` -vivosnap.rb prepare self-editor-account [VIVO_homepage_URL] [uri_list_file] [admin_email] [admin_password] [editor_email] [editor_password] +vivosnap.rb prepare self-editor-account [VIVO_homepage_URL] [uri_list_file] [admin_email:admin_password] [editor_email:editor_password] ``` -**NOT IMPLEMENTED** Write triples to the user accounts model of the VIVO to create the self-editor-account (unless it exists already) -and to make it a proxy editor for all of the URLs in the list. +and to make it a proxy editor for each of the URLs in the list. ``` vivosnap.rb prepare sub-list [session_list_file] [count] [sub_list_file] diff --git a/cmd_prepare_self_editor_account/cmd_prepare_self_editor_account.rb b/cmd_prepare_self_editor_account/cmd_prepare_self_editor_account.rb index e0b7ce7..e558345 100644 --- a/cmd_prepare_self_editor_account/cmd_prepare_self_editor_account.rb +++ b/cmd_prepare_self_editor_account/cmd_prepare_self_editor_account.rb @@ -1,10 +1,275 @@ +=begin +-------------------------------------------------------------------------------- + +Write triples to the user accounts model of the VIVO to create the +self-editor-account (unless it exists already), and to make it a proxy editor for +all of the URLs in the list. + +-------------------------------------------------------------------------------- + +Export the user-accounts model and parse it as RDF. +Note whether the editor account already exists, by email. + If so, + get the URI + Complain if not a SELF_EDITOR + Complain if not at least one login + Complain if the md5 password is not the one we were planning to use. + Otherwise, + Generate triples to create the account. + URI is + + a auth:UserAccount ; + auth:emailAddress "proxy@mydomain.edu"^^xsd:string ; + auth:firstName "Proxy"^^xsd:string ; + auth:hasPermissionSet auth:SELF_EDITOR ; + auth:lastName "Proxy"^^xsd:string ; + auth:loginCount "1"^^xsd:int ; + auth:md5password "DC647EB65E6711E155375218212B3964"^^xsd:string ; + auth:proxyEditorFor ; + auth:status "ACTIVE"^^xsd:string . + +-------------------------------------------------------------------------------- + +The proxy relationship + + ; + +-------------------------------------------------------------------------------- +=end +require 'cgi' +require 'rubygems' +require 'httpclient' +require 'rdf' +require 'rdf/raptor' +require 'tempfile' + class CmdPrepareSelfEditorAccount + include ::ArgsChecker + + USAGE = 'prepare self-editor-account [VIVO_homepage_URL] [uri_list_file] [admin_email:admin_password] [editor_email:editor_password]' + + USER_MODEL = 'http://vitro.mannlib.cornell.edu/default/vitro-kb-userAccounts' + + AUTH_EMAIL_ADDRESS = RDF::URI.new('http://vitro.mannlib.cornell.edu/ns/vitro/authorization#emailAddress') + AUTH_FIRST_NAME = RDF::URI.new('http://vitro.mannlib.cornell.edu/ns/vitro/authorization#firstName') + AUTH_HAS_PERMISSIONS = RDF::URI.new('http://vitro.mannlib.cornell.edu/ns/vitro/authorization#hasPermissionSet') + AUTH_LAST_NAME = RDF::URI.new('http://vitro.mannlib.cornell.edu/ns/vitro/authorization#lastName') + AUTH_LOGIN_COUNT = RDF::URI.new('http://vitro.mannlib.cornell.edu/ns/vitro/authorization#loginCount') + AUTH_MD5_PASSWORD = RDF::URI.new('http://vitro.mannlib.cornell.edu/ns/vitro/authorization#md5password') + AUTH_PROXY_EDITOR_FOR = RDF::URI.new('http://vitro.mannlib.cornell.edu/ns/vitro/authorization#proxyEditorFor') + AUTH_SELF_EDITOR = RDF::URI.new('http://vitro.mannlib.cornell.edu/ns/vitro/authorization#SELF_EDITOR') + AUTH_STATUS = RDF::URI.new('http://vitro.mannlib.cornell.edu/ns/vitro/authorization#status') + AUTH_USER_ACCOUNT = RDF::URI.new('http://vitro.mannlib.cornell.edu/ns/vitro/authorization#UserAccount') def initialize(args) - bogus "CmdPrepareSelfEditorAccount.initialize NOT IMPLEMENTED" + @args = args + + complain("usage: #{USAGE}") unless 4 == args.size + + @vivo_home_url = confirm_vivo_home_url(args[0]) + @uri_list_file = confirm_file_exists(args[1]) + @admin_email, @admin_password = split_credentials(args[2]) + @proxy_email, @proxy_password = split_credentials(args[3]) end - + + def split_credentials(arg) + complain("usage: #{USAGE}") unless 1 == arg.count(':') + arg.split(':') + end + def run() - bogus "CmdPrepareSelfEditorAccount.run NOT IMPLEMENTED" + look_for_existing_self_editor + prepare_upload_rdf_file + upload_rdf + report + end + + def look_for_existing_self_editor + export_user_model + parse_user_model + inspect_user_model end + + def export_user_model() + @session = HTTPClient.new + + login_parms = { 'email' => @admin_email, 'password' => @admin_password } + res = @session.get(add_to_home_url(@vivo_home_url, 'programLogin'), login_parms, nil, true) + raise UserInputError.new("Invalid admin credentials: #{@admin_email}:#{@admin_password}") unless res.status == 200 + + parms = {'action' => 'configModels', 'modelName' => USER_MODEL} + res = @session.get(add_to_home_url(@vivo_home_url, 'ingest'), parms, nil, true) + raise "Failed to show configModels." unless res.content.include?(USER_MODEL) + + export_parms = {'action' => 'outputModel', 'modelName' => USER_MODEL} + res = @session.get(add_to_home_url(@vivo_home_url, 'ingest'), export_parms, nil, true) + raise "Failed to export the User Accounts model." unless res.status == 200 + + @user_model_string = res.content + end + + def parse_user_model() + @user_model_graph = RDF::Graph() do |graph| + RDF::Reader.for(:turtle).new(@user_model_string) do |reader| + reader.each_statement do |statement| + graph << statement + end + end + end + end + + def inspect_user_model() + @editor_uri = nil + @create_editor = true + + editor_email_stmt = @user_model_graph.first([nil, AUTH_EMAIL_ADDRESS, RDF::Literal.new(@proxy_email)]) + return unless editor_email_stmt + + editor = editor_email_stmt.subject + + self_editor_stmt = @user_model_graph.first([editor, AUTH_HAS_PERMISSIONS, AUTH_SELF_EDITOR]) + raise "An account for #{@proxy_email} already exists, but is not a self-editor." unless self_editor_stmt + + login_count = @user_model_graph.first_literal([editor, AUTH_LOGIN_COUNT, nil]) + raise "An account for #{@proxy_email} already exists, but has never logged in." unless login_count && login_count.value.to_i > 0 + + md5_password = @user_model_graph.first_literal([editor, AUTH_MD5_PASSWORD, nil]) + raise "An account for #{@proxy_email} already exists, but the password is not '#{@proxy_password}'." unless md5_password && md5_password.to_s == encode_md5(@proxy_password) + + @editor_uri = editor.to_s + @create_editor = false + end + + def add_to_home_url(home_url, path) + if home_url.end_with?('/') + home_url + path + else + home_url + '/' + path + end + end + + def prepare_upload_rdf_file + insert_account_statements unless @editor_uri + insert_proxy_statements + write_graph_to_a_file + end + + def insert_account_statements() + @editor_uri = "http://vivosnap.proxy/#{Time.now.to_f}" + editor = RDF::URI.new(@editor_uri) + + @user_model_graph << RDF::Statement.new(editor, RDF::type, AUTH_USER_ACCOUNT) + @user_model_graph << RDF::Statement.new(editor, AUTH_EMAIL_ADDRESS, string_literal(@proxy_email)) + @user_model_graph << RDF::Statement.new(editor, AUTH_FIRST_NAME, string_literal('VIVOSNAP')) + @user_model_graph << RDF::Statement.new(editor, AUTH_HAS_PERMISSIONS, AUTH_SELF_EDITOR) + @user_model_graph << RDF::Statement.new(editor, AUTH_LAST_NAME, string_literal('PROXY')) + @user_model_graph << RDF::Statement.new(editor, AUTH_LOGIN_COUNT, RDF::Literal.new(1)) + @user_model_graph << RDF::Statement.new(editor, AUTH_MD5_PASSWORD, string_literal(encode_md5(@proxy_password))) + @user_model_graph << RDF::Statement.new(editor, AUTH_STATUS, string_literal('ACTIVE')) + end + + def insert_proxy_statements + @proxy_count = 0 + editor = RDF::URI.new(@editor_uri) + File.open(@uri_list_file) do |f| + f.each_line do |uri| + next if uri.start_with?('#') || uri.strip.empty? + @user_model_graph << RDF::Statement.new(editor, AUTH_PROXY_EDITOR_FOR, RDF::URI.new(uri.strip)) + @proxy_count += 1 + end + end + end + + def write_graph_to_a_file() + @temp_file = Tempfile.new('user_model') + RDF::Writer.for(:ntriples).new(@temp_file) do |writer| + @user_model_graph.each_statement do |statement| + writer << statement + end + end + @temp_file.rewind + end + + def upload_rdf + begin + parms = { 'action' => 'loadRDFData', 'modelName' => USER_MODEL, 'filePath' => @temp_file, 'language' => 'N-TRIPLE' } + res = @session.post(add_to_home_url(@vivo_home_url, 'uploadRDF'), parms) + raise "Failed to load the user model: status is #{res.status}." unless res.status == 200 + ensure + @temp_file.close + @temp_file.unlink + end + end + + def string_literal(s) + RDF::Literal.new(s, :datatype => RDF::XSD.string) + end + + def encode_md5(raw) + Digest::MD5.new.digest(raw).each_byte.map { |b| b.to_s(16) }.join.upcase + end + + def report() + puts + if @create_editor + puts "Created user account #{@proxy_email}:#{@proxy_password}" + else + puts "Found existing user account #{@proxy_email}:#{@proxy_password}" + end + puts "Set #{@proxy_count} auth:proxyEditorFor statements on #{@editor_uri}" + puts + end + end diff --git a/cmd_prepare_session_list/cmd_prepare_session_list.rb b/cmd_prepare_session_list/cmd_prepare_session_list.rb index bbd4a4c..64dea4b 100644 --- a/cmd_prepare_session_list/cmd_prepare_session_list.rb +++ b/cmd_prepare_session_list/cmd_prepare_session_list.rb @@ -28,35 +28,30 @@ def initialize(args) complain("usage: #{USAGE}") unless (1..3).include? args.size - @uri_list_file = args[0] - complain("'#{@uri_list_file}' does not exist.") unless File.exist?(@uri_list_file) + @uri_list_file = confirm_file_exists(args[0]) + @credential, @session_list_file = parse_remaining_args(args) + @output = set_output_io(@session_list_file, @replace) + end + def parse_remaining_args(args) case args.size when 1 - @credentials = nil - @session_list_file = nil + [nil, nil] when 2 if args[1].include?(':') - @credentials = args[1].split(':') - @session_list_file = nil + [split_credentials(args[1]), nil] else - @credentials = nil - @session_list_file = args[1] + [nil, args[1]] end - else - @credentials = args[1].split(':') - @session_list_file = args[2] - end - - complain("usage: #{USAGE}") if @credentials && @credentials.size != 2 - - if (@session_list_file) - complain("#{@session_list_file} already exists. Specify REPLACE to overwrite.") if File.exist?(@session_list_file) unless @replace - @output = File.open(@session_list_file, 'w') - else - @output = $stdout + else # 3 + [split_credentials(args[1]), args[2]] end end + + def split_credentials(arg) + complain("usage: #{USAGE}") unless 1 == arg.count(':') + arg.split(':') + end def run() write_heading diff --git a/cmd_prepare_uri_list/cmd_prepare_uri_list.rb b/cmd_prepare_uri_list/cmd_prepare_uri_list.rb index f14c273..a06fd62 100644 --- a/cmd_prepare_uri_list/cmd_prepare_uri_list.rb +++ b/cmd_prepare_uri_list/cmd_prepare_uri_list.rb @@ -13,31 +13,13 @@ class CmdPrepareUriList USAGE = 'prepare uri-list [class_list_file] [VIVO_homepage_URL] {uri_list_file {REPLACE}}' def initialize(args) @args = args - @replace = args.delete('REPLACE') + complain("usage: #{USAGE}") unless (2..3).include? args.size - @class_list_file = args[0] - complain("'#{@class_list_file}' does not exist.") unless File.exist?(@class_list_file) - - @vivo_home_url = args[1] - begin - HttpRequest.new(@vivo_home_url).exec do |response| - complain("Response from '#{@vivo_home_url}' does not look like VIVO's home page.") unless response.body =~ /body class="home"/ - end - rescue - puts "#{$!}\n#{$!.backtrace.join("\n")}" - complain("Can't contact VIVO at '#{@vivo_home_url}': #{$!}") - end - - @uri_list_file = args[2] - - if @uri_list_file - complain("'#{@uri_list_file}' already exists. Specify REPLACE to replace it.") if File.exist?(@uri_list_file) unless @replace - @output = File.open(@uri_list_file, 'w') - else - @output = $stdout - end + @class_list_file = confirm_file_exists(args[0]) + @vivo_home_url = confirm_vivo_home_url(args[1]) + @output = set_output_io(args[2], @replace) end def run() diff --git a/common.rb b/common.rb index 2bc4059..6829731 100644 --- a/common.rb +++ b/common.rb @@ -43,6 +43,11 @@ def close $stdout = MultiIO.new(STDOUT, File.open(File.expand_path('~/vivosnap.stdout'), "w")) $stderr = MultiIO.new(STDERR, File.open(File.expand_path('~/vivosnap.stderr'), "w")) +require 'rdf' + +require_relative 'utils/args_checker' +require_relative 'utils/http_request' + require_relative 'cmd_prepare_uri_list/cmd_prepare_uri_list' require_relative 'cmd_prepare_session_list/cmd_prepare_session_list' require_relative 'cmd_prepare_self_editor_account/cmd_prepare_self_editor_account' @@ -51,4 +56,3 @@ def close require_relative 'cmd_compare/cmd_compare' require_relative 'cmd_compare_again/cmd_compare_again' require_relative 'cmd_display/cmd_display' -require_relative 'utils/http_request' diff --git a/utils/args_checker.rb b/utils/args_checker.rb new file mode 100644 index 0000000..2ec17d7 --- /dev/null +++ b/utils/args_checker.rb @@ -0,0 +1,52 @@ +=begin + +Some handy methods for checking the validity of command line arguments. + +=end + +module ArgsChecker + # + # Just return the path, if the file exists. Otherwise, complain. + # + def confirm_file_exists(path) + if File.exist?(path) + path + else + complain("'#{path}' does not exist.") + end + end + + # + # Check that this URL will give us a response, and that the response looks + # at least a little like the VIVO home page. + # + # If everything is OK, return the URL + # + def confirm_vivo_home_url(url) + begin + HttpRequest.new(url).exec do |response| + complain("Response from '#{url}' does not look like VIVO's home page.") unless response.body =~ /body class="home"/ + end + rescue + complain("Can't contact VIVO at '#{url}': #{$!}") + end + + url + end + + # + # If the file is not specified, use standard out. + # If the file exists and replace is not specified, complain. + # Otherwise, open the file for writing. + # + # In any case, return the output IO. + # + def set_output_io(path, replace) + if path + complain("'#{path}' already exists. Specify REPLACE to replace it.") if File.exist?(path) unless replace + File.open(path, 'w') + else + $stdout + end + end +end \ No newline at end of file diff --git a/utils/http_request.rb b/utils/http_request.rb index 8a7928e..a5beb6d 100644 --- a/utils/http_request.rb +++ b/utils/http_request.rb @@ -38,8 +38,15 @@ def exec() begin http.request(request) do |response| - response.value - yield response if block_given? + if response.kind_of? Net::HTTPRedirection + new_url = response['location'] + HttpRequest.new(new_url, @method).headers(@headers).parameters(@parameters).exec do |r| + yield r if block_given? + end + else + response.value + yield response if block_given? + end end rescue Net::HTTPServerException => e raise e.exception(e.message << "\nProblem request: \n#{inspect_request(request, @url)}") @@ -59,11 +66,11 @@ def add_parameters(request_uri) end end - + def inspect_request(r, url) headers = r.to_hash.to_a.map{|h| " #{h[0]} ==> #{h[1]}"}.join("\n") body = body ? CGI.unescape(r.body) : 'NO BODY' "#{r.method} #{url}\n#{headers}\n#{body}" end -end \ No newline at end of file +end