google_apps_oauth2 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'google_apps_oauth2'
3
+ spec.version = '0.1'
4
+ spec.license = "MIT"
5
+ spec.summary = 'Google Apps APIs using OAuth2'
6
+ spec.description = 'Library for interfacing with Google Apps Domain and Application APIs via OAuth2'
7
+ spec.authors = ['Will Read']
8
+ spec.files = Dir.glob(File.join('**', 'lib', '**', '*.rb'))
9
+ spec.homepage = 'https://github.com/TildeWill/google_apps'
10
+
11
+ spec.add_dependency('libxml-ruby', '>= 2.2.2')
12
+ spec.add_dependency('httparty')
13
+
14
+ spec.add_development_dependency 'rake'
15
+ spec.add_development_dependency 'rspec'
16
+ spec.add_development_dependency 'webmock'
17
+
18
+ spec.files = `git ls-files`.split("\n")
19
+ spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ end
@@ -0,0 +1,7 @@
1
+ require 'google_apps_oauth2/parsers/feed_parser'
2
+ require 'google_apps_oauth2/transport'
3
+ require 'google_apps_oauth2/atom/atom'
4
+ require 'google_apps_oauth2/atom/node'
5
+ require 'google_apps_oauth2/atom/document'
6
+ require 'google_apps_oauth2/atom/feed'
7
+ require 'google_apps_oauth2/atom/user'
@@ -0,0 +1,26 @@
1
+ require 'libxml'
2
+
3
+ module GoogleAppsOauth2
4
+ module Atom
5
+ include LibXML
6
+
7
+ NAMESPACES = {
8
+ atom: 'http://www.w3.org/2005/Atom',
9
+ apps: 'http://schemas.google.com/apps/2006',
10
+ gd: 'http://schemas.google.com/g/2005',
11
+ openSearch: 'http://a9.com/-/spec/opensearchrss/1.0/'
12
+ }
13
+
14
+ CATEGORY = {
15
+ user: [['scheme', 'http://schemas.google.com/g/2005#kind'], ['term', 'http://schemas.google.com/apps/2006#user']],
16
+ }
17
+
18
+ ENTRY_TAG = ["<atom:entry xmlns:atom=\"#{NAMESPACES[:atom]}\" xmlns:apps=\"#{NAMESPACES[:apps]}\" xmlns:gd=\"#{NAMESPACES[:gd]}\">", '</atom:entry>']
19
+
20
+ def user(*args)
21
+ User.new *args
22
+ end
23
+
24
+ module_function :user
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ module GoogleAppsOauth2
2
+ module Atom
3
+ class Document
4
+ include Node
5
+
6
+ def initialize(doc, map = {})
7
+ @doc = parse(doc)
8
+ @map = map
9
+ end
10
+
11
+ def parse(xml)
12
+ document = Atom::XML::Document.string(xml)
13
+ Atom::XML::Parser.document(document).parse
14
+ end
15
+
16
+ # find_values searches @doc and assigns any values
17
+ # to their corresponding instance variables. This is
18
+ # useful when we've been given a string of XML and need
19
+ # internal consistency in the object.
20
+ #
21
+ # find_values
22
+ def find_values
23
+ @doc.root.each do |entry|
24
+ intersect = @map.keys & entry.attributes.to_h.keys.map(&:to_sym)
25
+ set_instances(intersect, entry) unless intersect.empty?
26
+ end
27
+ end
28
+
29
+ # Sets instance variables in the current object based on
30
+ # values found in the XML document and the mapping specified
31
+ # in GoogleApps::Atom::MAPS
32
+
33
+ # @param [Array] intersect
34
+ # @param [LibXML::XML::Node] node
35
+ # @param [Hash] map
36
+ #
37
+ # @visibility public
38
+ # @return
39
+ def set_instances(intersect, node)
40
+ intersect.each do |attribute|
41
+ instance_variable_set "@#{@map[attribute]}", check_value(node.attributes[attribute])
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,90 @@
1
+ module GoogleAppsOauth2
2
+ module Atom
3
+ class Feed < Document
4
+ attr_reader :doc, :items, :next_page
5
+ def initialize(body)
6
+ super(body)
7
+
8
+ @items = entries_from(document: @doc, entry_tag: 'entry')
9
+ end
10
+
11
+ def entries_from(properties)
12
+ properties[:document].root.inject([]) do |results, entry|
13
+ if entry.name == properties[:entry_tag]
14
+ results << new_doc(node_to_ary(entry), ['apps:', 'atom:', 'gd:'])
15
+ end
16
+ set_next_page(entry) if entry.name == 'link' and entry.attributes[:rel] == 'next'
17
+ results
18
+ end
19
+ end
20
+
21
+ def set_next_page(node)
22
+ @next_page = node.attributes[:href]
23
+ end
24
+
25
+ # node_to_ary converts a Atom::XML::Node to an array.
26
+ #
27
+ # node_to_ary node
28
+ #
29
+ # node_to_ary returns the string representation of the
30
+ # given node split on \n.
31
+ def node_to_ary(node)
32
+ node.to_s.split("\n")
33
+ end
34
+
35
+ # new_doc creates a new Atom document from the data
36
+ # provided in the feed. new_doc takes a type, an
37
+ # array of content to be placed into the document
38
+ # as well as an array of filters.
39
+ #
40
+ # new_doc 'user', content_array, ['apps:']
41
+ #
42
+ # new_doc returns an GoogleApps::Atom document of the
43
+ # specified type.
44
+ def new_doc(content_array, filters)
45
+ content_array = filters.map do |filter|
46
+ grab_elements(content_array, filter)
47
+ end
48
+
49
+ add_category(content_array)
50
+
51
+ Atom.send :user, entry_wrap(content_array.flatten).join("\n")
52
+ end
53
+
54
+ # add_category adds the proper atom:category node to the
55
+ # content_array
56
+ #
57
+ # add_category content_array, 'user'
58
+ #
59
+ # add_category returns the modified content_array
60
+ def add_category(content_array)
61
+ content_array.unshift(create_node(type: 'atom:category', attrs: Atom::CATEGORY[:user]).to_s)
62
+ end
63
+
64
+ # grab_elements applies the specified filter to the
65
+ # provided array. Google's feed provides a lot of data
66
+ # that we don't need in an entry document.
67
+ #
68
+ # grab_elements content_array, 'apps:'
69
+ #
70
+ # grab_elements returns an array of items from content_array
71
+ # that match the given filter.
72
+ def grab_elements(content_array, filter)
73
+ content_array.grep(Regexp.new filter)
74
+ end
75
+
76
+ # entry_wrap adds atom:entry opening and closing tags
77
+ # to the provided content_array and the beginning and
78
+ # end.
79
+ #
80
+ # entry_wrap content_array
81
+ #
82
+ # entry_wrap returns an array with an opening atom:entry
83
+ # element prepended to the front and a closing atom:entry
84
+ # tag appended to the end.
85
+ def entry_wrap(content_array)
86
+ content_array.unshift(Atom::ENTRY_TAG[0]).push(Atom::ENTRY_TAG[1])
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,57 @@
1
+ module GoogleAppsOauth2
2
+ module Atom
3
+ module Node
4
+
5
+ # create_node takes a hash of properties from which to
6
+ # build the XML node. The properties hash must have
7
+ # a :type key, it is also possible to pass an :attrs
8
+ # key with an array of attribute name, value pairs.
9
+ #
10
+ # create_node type: 'apps:property', attrs: [['name', 'Tim'], ['userName', 'tim@bob.com']]
11
+ #
12
+ # create_node returns an Atom::XML::Node with the specified
13
+ # properties.
14
+ def create_node(properties)
15
+ if properties[:attrs]
16
+ add_attributes Atom::XML::Node.new(properties[:type]), properties[:attrs]
17
+ else
18
+ Atom::XML::Node.new properties[:type]
19
+ end
20
+ end
21
+
22
+
23
+ # add_attributes adds the specified attributes to the
24
+ # given node. It takes a LibXML::XML::Node and an
25
+ # array of name, value attribute pairs.
26
+ #
27
+ # add_attribute node, [['title', 'emperor'], ['name', 'Napoleon']]
28
+ #
29
+ # add_attribute returns the modified node.
30
+ def add_attributes(node, attributes)
31
+ attributes.each do |attribute|
32
+ node.attributes[attribute[0]] = attribute[1]
33
+ end
34
+
35
+ node
36
+ end
37
+
38
+ # Returns true if "true" and false if "false"
39
+
40
+ #
41
+ # @param [String] value
42
+ #
43
+ # @visibility public
44
+ # @return
45
+ def check_value(value)
46
+ case value
47
+ when 'true'
48
+ true
49
+ when 'false'
50
+ false
51
+ else
52
+ value
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,21 @@
1
+ module GoogleAppsOauth2
2
+ module Atom
3
+ class User < Document
4
+ attr_reader :doc, :login, :suspended, :first_name, :last_name, :quota, :password
5
+
6
+ MAP = {
7
+ userName: :login,
8
+ suspended: :suspended,
9
+ familyName: :last_name,
10
+ givenName: :first_name,
11
+ limit: :quota,
12
+ password: :password
13
+ }
14
+
15
+ def initialize(xml)
16
+ super(xml, MAP)
17
+ find_values
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ require 'httparty'
2
+
3
+ module GoogleAppsOauth2
4
+ module Parsers
5
+ class FeedParser < HTTParty::Parser
6
+ SupportedFormats = {"application/atom+xml" => :atom}
7
+
8
+ protected
9
+
10
+ def atom
11
+ GoogleAppsOauth2::Atom::Feed.new(body).items
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,57 @@
1
+ require 'cgi'
2
+ require 'openssl'
3
+ require 'rexml/document'
4
+ require 'httparty'
5
+
6
+ module GoogleAppsOauth2
7
+ class Transport
8
+ include HTTParty
9
+
10
+ parser GoogleAppsOauth2::Parsers::FeedParser
11
+
12
+ base_uri 'https://apps-apis.google.com/a/feeds'
13
+
14
+ def initialize(options, &block)
15
+ @domain = options[:domain]
16
+ @token = options[:token]
17
+ @client_id = options[:client_id]
18
+ @client_secret = options[:client_secret]
19
+ @refresh_token = options[:refresh_token]
20
+ @token_changed_block = block
21
+
22
+ @user_path= "/#{@domain}/user/2.0"
23
+ end
24
+
25
+ def get_users(options = {})
26
+ headers = {'content-type' => 'application/atom+xml', 'Authorization' => "OAuth #{token}"}
27
+
28
+ url = user_path + "?startUsername=#{options[:start]}"
29
+ response = self.class.get(url, headers: headers)
30
+ response = check_for_refresh(response)
31
+
32
+ response
33
+ end
34
+
35
+ private
36
+ attr_reader :user_path
37
+ attr_reader :domain, :token, :refresh_token, :client_id, :client_secret
38
+
39
+ def check_for_refresh(old_response)
40
+ response = old_response
41
+ if old_response.code == 401
42
+ data = {
43
+ :client_id => @client_id,
44
+ :client_secret => @client_secret,
45
+ :refresh_token => refresh_token,
46
+ :grant_type => "refresh_token"
47
+ }
48
+ response_json = MultiJson.load(self.class.post("https://accounts.google.com/o/oauth2/token", :body => data))
49
+ @token = response_json["access_token"]
50
+ @token_changed_block.call(@token)
51
+ headers = old_response.request.options[:headers].merge("Authorization" => "OAuth #{token}")
52
+ response = self.class.get(old_response.request.uri.to_s, headers: headers)
53
+ end
54
+ response
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,10 @@
1
+ <HTML>
2
+ <HEAD>
3
+ <TITLE>Token invalid - Invalid token: Stateless token expired</TITLE>
4
+ </HEAD>
5
+ <BODY BGCOLOR="#FFFFFF" TEXT="#000000">
6
+ <H1>Token invalid - Invalid token: Stateless token expired</H1>
7
+
8
+ <H2>Error 401</H2>
9
+ </BODY>
10
+ </HTML>
@@ -0,0 +1,6 @@
1
+ {
2
+ "access_token": "some-new-token",
3
+ "token_type": "Bearer",
4
+ "expires_in": 3600,
5
+ "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjNmNDUwOTU1YzA3ZGUyZWFhNDA1OTg1NmMzOGFmYWY1OTFmMWUxYTcifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiY2lkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiaGQiOiJsb29wYi5hYyIsInRva2VuX2hhc2giOiI3RnV3YXBnV1dXaWxHc1l6dm9SZURBIiwiYXRfaGFzaCI6IjdGdXdhcGdXV1dpbEdzWXp2b1JlREEiLCJ2ZXJpZmllZF9lbWFpbCI6InRydWUiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6IndpbGxAbG9vcGIuYWMiLCJpZCI6IjEwMjY2MDg3NTAyMjk1NTU4OTU2MCIsInN1YiI6IjEwMjY2MDg3NTAyMjk1NTU4OTU2MCIsImF1ZCI6IjQwNzQwODcxODE5Mi5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImlhdCI6MTM2MTQ5ODU5OSwiZXhwIjoxMzYxNTAyNDk5fQ.HblnwmqqyQoapWIZP90QqB92MBTUQEnYRjvFRO8BaWV2VVQIgc2v1Vb_oOaChNs3w2vXk-2NQiJlWVdqDFARCRe5n3BID2wr8iwSUzVCEt4fg3rqxQ-IeXXWNYCx-32N3k7w4mcnxsnglw7ajol4vkv-2Nx8peA3Zvwt0NXdxj8"
6
+ }
@@ -0,0 +1,5 @@
1
+ <atom:entry xmlns:atom="http://www.w3.org/2005/Atom" xmlns:apps="http://schemas.google.com/apps/2006">
2
+ <atom:category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/apps/2006#user"/>
3
+ <apps:login userName="lholcomb2" suspended="false"/>
4
+ <apps:name familyName="Holcomb" givenName="Lawrence"/>
5
+ </atom:entry>
@@ -0,0 +1,49 @@
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
3
+ xmlns:apps='http://schemas.google.com/apps/2006' xmlns:gd='http://schemas.google.com/g/2005'>
4
+ <id>https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0</id>
5
+ <updated>1970-01-01T00:00:00.000Z</updated>
6
+ <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/apps/2006#user'/>
7
+ <title type='text'>Users</title>
8
+ <link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml'
9
+ href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0'/>
10
+ <link rel='http://schemas.google.com/g/2005#post' type='application/atom+xml'
11
+ href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0'/>
12
+ <link rel='self' type='application/atom+xml' href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0'/>
13
+ <link rel='next' type='application/atom+xml' href='https://apps-apis.google.com/a/feeds/cnm.edu/user/2.0?startUsername=aadams37'/>
14
+ <openSearch:startIndex>1</openSearch:startIndex>
15
+ <entry>
16
+ <id>https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0/misterunfriendly</id>
17
+ <updated>1970-01-01T00:00:00.000Z</updated>
18
+ <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/apps/2006#user'/>
19
+ <title type='text'>misterunfriendly</title>
20
+ <link rel='self' type='application/atom+xml'
21
+ href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0/misterunfriendly'/>
22
+ <link rel='edit' type='application/atom+xml'
23
+ href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0/misterunfriendly'/>
24
+ <apps:login userName='misterunfriendly' suspended='false' ipWhitelisted='false' admin='false'
25
+ changePasswordAtNextLogin='true' agreedToTerms='true'/>
26
+ <apps:quota limit='25600'/>
27
+ <apps:name familyName='Unfriendly' givenName='Mister'/>
28
+ <gd:feedLink rel='http://schemas.google.com/apps/2006#user.nicknames'
29
+ href='https://apps-apis.google.com/a/feeds/loopb.ac/nickname/2.0?username=misterunfriendly'/>
30
+ <gd:feedLink rel='http://schemas.google.com/apps/2006#user.emailLists'
31
+ href='https://apps-apis.google.com/a/feeds/loopb.ac/emailList/2.0?recipient=misterunfriendly%40loopb.ac'/>
32
+ </entry>
33
+ <entry>
34
+ <id>https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0/will</id>
35
+ <updated>1970-01-01T00:00:00.000Z</updated>
36
+ <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/apps/2006#user'/>
37
+ <title type='text'>will</title>
38
+ <link rel='self' type='application/atom+xml' href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0/will'/>
39
+ <link rel='edit' type='application/atom+xml' href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0/will'/>
40
+ <apps:login userName='will' suspended='false' ipWhitelisted='false' admin='true' changePasswordAtNextLogin='false'
41
+ agreedToTerms='true'/>
42
+ <apps:quota limit='25600'/>
43
+ <apps:name familyName='Read' givenName='Will'/>
44
+ <gd:feedLink rel='http://schemas.google.com/apps/2006#user.nicknames'
45
+ href='https://apps-apis.google.com/a/feeds/loopb.ac/nickname/2.0?username=will'/>
46
+ <gd:feedLink rel='http://schemas.google.com/apps/2006#user.emailLists'
47
+ href='https://apps-apis.google.com/a/feeds/loopb.ac/emailList/2.0?recipient=will%40loopb.ac'/>
48
+ </entry>
49
+ </feed>
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe GoogleAppsOauth2::Atom::Document do
4
+ let (:document) { GoogleAppsOauth2::Atom::Document.new File.read('spec/fixture_xml/user.xml') }
5
+ let (:doc_string) { File.read('spec/fixture_xml/users_feed.xml') }
6
+
7
+ describe "#parse" do
8
+ it "parses the given XML document" do
9
+ document.parse(doc_string).should be_a LibXML::XML::Document
10
+ end
11
+ end
12
+
13
+ describe "#to_s" do
14
+ it "Outputs @doc as a string" do
15
+ document.to_s.should be_a String
16
+ end
17
+ end
18
+ end