google_apps_oauth2 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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