diff --git a/app/controllers/mozilla_iam/admin/group_mappings_controller.rb b/app/controllers/mozilla_iam/admin/group_mappings_controller.rb index 0193348..f31b28f 100644 --- a/app/controllers/mozilla_iam/admin/group_mappings_controller.rb +++ b/app/controllers/mozilla_iam/admin/group_mappings_controller.rb @@ -11,7 +11,6 @@ module MozillaIAM def create mapping = GroupMapping.new(group_mappings_params) - mapping.authoritative = false if params[:authoritative].nil? mapping.group = Group.find_by(name: params[:group_name]) mapping.save! render json: success_json @@ -40,8 +39,7 @@ module MozillaIAM def group_mappings_params params.permit( :id, - :iam_group_name, - :authoritative + :iam_group_name ) end diff --git a/app/serializers/mozilla_iam/group_mapping_serializer.rb b/app/serializers/mozilla_iam/group_mapping_serializer.rb index 489ac64..beb56dc 100644 --- a/app/serializers/mozilla_iam/group_mapping_serializer.rb +++ b/app/serializers/mozilla_iam/group_mapping_serializer.rb @@ -2,8 +2,7 @@ module MozillaIAM class GroupMappingSerializer < ApplicationSerializer attributes :id, :group_name, - :iam_group_name, - :authoritative + :iam_group_name def group_name object.group.name diff --git a/assets/javascripts/discourse/models/mapping.js.es6 b/assets/javascripts/discourse/models/mapping.js.es6 index ab0e250..9102507 100644 --- a/assets/javascripts/discourse/models/mapping.js.es6 +++ b/assets/javascripts/discourse/models/mapping.js.es6 @@ -4,8 +4,7 @@ const Mapping = Ember.Object.extend({ asJSON () { return { group_name: this.get('group_name'), - iam_group_name: this.get('iam_group_name'), - authoritative: this.get('authoritative') + iam_group_name: this.get('iam_group_name') } }, diff --git a/assets/javascripts/discourse/templates/admin-plugins-mozilla-iam-mappings-edit.hbs b/assets/javascripts/discourse/templates/admin-plugins-mozilla-iam-mappings-edit.hbs index 8d5d956..f8c0ce5 100644 --- a/assets/javascripts/discourse/templates/admin-plugins-mozilla-iam-mappings-edit.hbs +++ b/assets/javascripts/discourse/templates/admin-plugins-mozilla-iam-mappings-edit.hbs @@ -10,11 +10,6 @@ {{text-field name='iam_group_name' value=model.iam_group_name}} -
- - {{input type='checkbox' name='authoritative' checked=model.authoritative}} -
-
diff --git a/assets/javascripts/discourse/templates/admin-plugins-mozilla-iam-mappings-index.hbs b/assets/javascripts/discourse/templates/admin-plugins-mozilla-iam-mappings-index.hbs index e5b92cb..6031dbe 100644 --- a/assets/javascripts/discourse/templates/admin-plugins-mozilla-iam-mappings-index.hbs +++ b/assets/javascripts/discourse/templates/admin-plugins-mozilla-iam-mappings-index.hbs @@ -4,7 +4,6 @@ ID Discourse Group IAM Group - Authoritative? @@ -13,7 +12,6 @@ {{#link-to 'adminPlugins.mozilla-iam.mappings.edit' mapping}}{{mapping.id}}{{/link-to}} {{mapping.group_name}} {{mapping.iam_group_name}} - {{mapping.authoritative}} {{/each}} diff --git a/assets/javascripts/discourse/templates/connectors/admin-user-details/mozilla-iam.hbs b/assets/javascripts/discourse/templates/connectors/admin-user-details/mozilla-iam.hbs new file mode 100644 index 0000000..c21dc9a --- /dev/null +++ b/assets/javascripts/discourse/templates/connectors/admin-user-details/mozilla-iam.hbs @@ -0,0 +1,26 @@ +
+

Mozilla IAM

+ +
+
User ID
+
+ {{#if model.mozilla_iam.uid}} + {{model.mozilla_iam.uid}} + {{else}} + — + {{/if}} +
+
+ +
+
Last Refresh
+
+ {{#if model.mozilla_iam.last_refresh}} + {{model.mozilla_iam.last_refresh}} + {{else}} + — + {{/if}} +
+
+ +
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8b2d7ed..ef0bcc8 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4,6 +4,7 @@ en: site_settings: categories: auth0: 'Auth0' + mozilla_iam: 'Mozilla IAM' mozilla_iam: mappings: title: 'IAM Group Mappings' diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index c0f97c3..0d886ac 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -3,3 +3,5 @@ en: auth0_domain: 'You auth0 domain. This is typically .auth0.com.' auth0_client_id: 'Your auth0 client id. Find it on the application settings on the Auth0 dashboard.' auth0_client_secret: 'Your auth0 client secret. Find it on the application settings on the Auth0 dashboard.' + mozilla_iam_person_api_url: 'URL to the Mozilla IAM Person API endpoint' + mozilla_iam_person_api_aud: 'Audience for the Mozilla IAM Person API' diff --git a/config/settings.yml b/config/settings.yml index 61e3bec..1c13f3e 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -8,3 +8,10 @@ auth0: auth0_client_secret: default: '' shadowed_by_global: true +mozilla_iam: + mozilla_iam_person_api_url: + default: 'https://uhbz4h3wa8.execute-api.us-west-2.amazonaws.com/prod' + shadowed_by_global: true + mozilla_iam_person_api_aud: + default: 'https://person-api.sso.mozilla.com' + shadowed_by_global: true diff --git a/db/migrate/20171219175923_remove_authoritative_from_mozilla_iam_group_mappings.rb b/db/migrate/20171219175923_remove_authoritative_from_mozilla_iam_group_mappings.rb new file mode 100644 index 0000000..e632d60 --- /dev/null +++ b/db/migrate/20171219175923_remove_authoritative_from_mozilla_iam_group_mappings.rb @@ -0,0 +1,5 @@ +class RemoveAuthoritativeFromMozillaIAMGroupMappings < ActiveRecord::Migration[5.1] + def change + remove_column :mozilla_iam_group_mappings, :authoritative, :boolean + end +end diff --git a/lib/mozilla_iam.rb b/lib/mozilla_iam.rb index f588ea7..1d94b80 100644 --- a/lib/mozilla_iam.rb +++ b/lib/mozilla_iam.rb @@ -1,6 +1,8 @@ require_relative 'mozilla_iam/engine' require_relative 'mozilla_iam/api' +require_relative 'mozilla_iam/person_api' +require_relative 'mozilla_iam/management_api' require_relative 'mozilla_iam/application_extensions' require_relative 'mozilla_iam/authenticator' require_relative 'mozilla_iam/jwks' diff --git a/lib/mozilla_iam/api.rb b/lib/mozilla_iam/api.rb index 781d78e..a8bfe1f 100644 --- a/lib/mozilla_iam/api.rb +++ b/lib/mozilla_iam/api.rb @@ -1,74 +1,75 @@ module MozillaIAM class API - class << self + def initialize(config) + @client_id = config[:client_id] || SiteSetting.auth0_client_id + @client_secret = config[:client_secret] || SiteSetting.auth0_client_secret + @token_endpoint = config[:token_endpoint] || "https://#{SiteSetting.auth0_domain}/oauth/token" + @url = config[:url] + raise ArgumentError, "no url in config" unless @url + @aud = config[:aud] + raise ArgumentError, "no aud in config" unless @aud + end - def user(uid) - Rails.logger.info("Auth0 API query for user_id: #{uid}") - profile = get("users/#{uid}", fields: 'app_metadata') - { app_metadata: {} }.merge(profile)[:app_metadata] + private + + def get(path, params = false) + path = URI.encode(path) + uri = URI("#{@url}/#{path}") + uri.query = URI.encode_www_form(params) if params + + req = Net::HTTP::Get.new(uri) + req['Authorization'] = "Bearer #{access_token}" + + res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| + http.request(req) end - private - - def get(path, params = false) - path = URI.encode(path) - uri = URI("https://#{SiteSetting.auth0_domain}/api/v2/#{path}") - uri.query = URI.encode_www_form(params) if params - - req = Net::HTTP::Get.new(uri) - req['Authorization'] = "Bearer #{access_token}" - - res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| - http.request(req) - end - - if res.code == '200' - MultiJson.load(res.body, symbolize_keys: true) - else - {} - end + if res.code == '200' + MultiJson.load(res.body, symbolize_keys: true) + else + {} end + end - def access_token - api_creds = ::PluginStore.get('mozilla-iam', 'api_creds') - if api_creds.nil? || api_creds[:exp] < Time.now.to_i + 60 - refresh_token - else - api_creds[:access_token] - end + def access_token + api_token = ::PluginStore.get('mozilla-iam', "#{@aud}_token") + if api_token.nil? || api_token[:exp] < Time.now.to_i + 60 + refresh_token + else + api_token[:access_token] end + end - def refresh_token - token = fetch_token - payload = verify_token(token) - ::PluginStore.set('mozilla-iam', 'api_creds', { access_token: token, exp: payload['exp'] }) - token - end + def refresh_token + token = fetch_token + payload = verify_token(token) + ::PluginStore.set('mozilla-iam', "#{@aud}_token", { access_token: token, exp: payload['exp'] }) + token + end - def fetch_token - response = - Faraday.post( - 'https://' + SiteSetting.auth0_domain + '/oauth/token', - { - grant_type: 'client_credentials', - client_id: SiteSetting.auth0_client_id, - client_secret: SiteSetting.auth0_client_secret, - audience: 'https://' + SiteSetting.auth0_domain + '/api/v2/' - } - ) - MultiJson.load(response.body)['access_token'] - end + def fetch_token + response = + Faraday.post( + @token_endpoint, + { + grant_type: 'client_credentials', + client_id: @client_id, + client_secret: @client_secret, + audience: @aud + } + ) + MultiJson.load(response.body)['access_token'] + end - def verify_token(token) - payload, header = - JWT.decode( - token, - aud: 'https://' + SiteSetting.auth0_domain + '/api/v2/', - sub: SiteSetting.auth0_client_id + '@clients', - verify_sub: true - ) - payload - end + def verify_token(token) + payload, header = + JWT.decode( + token, + aud: @aud, + sub: @client_id + '@clients', + verify_sub: true + ) + payload end end end diff --git a/lib/mozilla_iam/management_api.rb b/lib/mozilla_iam/management_api.rb new file mode 100644 index 0000000..ba7d577 --- /dev/null +++ b/lib/mozilla_iam/management_api.rb @@ -0,0 +1,17 @@ +module MozillaIAM + class ManagementAPI < API + + def initialize(config={}) + config = { + url: "https://#{SiteSetting.auth0_domain}/api/v2", + aud: "https://#{SiteSetting.auth0_domain}/api/v2/" + }.merge(config) + super(config) + end + + def profile(uid) + get("users/#{uid}") + end + + end +end diff --git a/lib/mozilla_iam/person_api.rb b/lib/mozilla_iam/person_api.rb new file mode 100644 index 0000000..4eeacfd --- /dev/null +++ b/lib/mozilla_iam/person_api.rb @@ -0,0 +1,18 @@ +module MozillaIAM + class PersonAPI < API + + def initialize(config={}) + config = { + url: SiteSetting.mozilla_iam_person_api_url, + aud: SiteSetting.mozilla_iam_person_api_aud + }.merge(config) + super(config) + end + + def profile(uid) + profile = get("profile/#{uid}") + MultiJson.load(profile[:body], symbolize_keys: true) + end + + end +end diff --git a/lib/mozilla_iam/profile.rb b/lib/mozilla_iam/profile.rb index aa28700..4290b2a 100644 --- a/lib/mozilla_iam/profile.rb +++ b/lib/mozilla_iam/profile.rb @@ -23,10 +23,14 @@ module MozillaIAM end end + def groups + Array(mgmt_profile[:groups]) | Array(mgmt_profile.dig(:app_metadata, :groups)) + end + private - def profile - @profile ||= API.user(@uid) + def mgmt_profile + @mgmt_profile ||= ManagementAPI.new.profile(@uid) end def last_refresh @@ -47,16 +51,7 @@ module MozillaIAM def update_groups GroupMapping.all.each do |mapping| - if mapping.authoritative - in_group = - profile[:authoritativeGroups]&.any? do |authoritative_group| - authoritative_group[:name] == mapping.iam_group_name - end - else - in_group = profile[:groups]&.include?(mapping.iam_group_name) - end - - if in_group + if groups.include?(mapping.iam_group_name) add_to_group(mapping.group) else remove_from_group(mapping.group) diff --git a/plugin.rb b/plugin.rb index 7a893e7..6eae3ae 100644 --- a/plugin.rb +++ b/plugin.rb @@ -1,6 +1,6 @@ # name: mozilla-iam # about: A plugin to integrate Discourse with Mozilla's Identity and Access Management (IAM) system -# version: 0.1.1 +# version: 0.1.3 # authors: Leo McArdle # url: https://github.com/mozilla/discourse-mozilla-iam @@ -22,3 +22,15 @@ auth_provider(title: 'Mozilla', message: 'Log In / Sign Up', authenticator: MozillaIAM::Authenticator.new('auth0', trusted: true), full_screen_login: true) + +after_initialize do + + add_to_serializer(:AdminDetailedUser, :mozilla_iam, false) do + object.custom_fields.select do |k, v| + k.start_with?('mozilla_iam') + end.map do |k, v| + [k.sub('mozilla_iam_', ''), v] + end.to_h + end + +end diff --git a/spec/components/mozilla_iam/api_spec.rb b/spec/components/mozilla_iam/api_spec.rb new file mode 100644 index 0000000..195ac2c --- /dev/null +++ b/spec/components/mozilla_iam/api_spec.rb @@ -0,0 +1,152 @@ +require_relative "../../iam_helper" + +describe MozillaIAM::API do + let(:config) do + return { + client_id: "abc", + client_secret: "def", + token_endpoint: "https://example.com/oauth/token", + url: "https://example.com/api", + aud: "example.com" + } + end + + let(:api) { MozillaIAM::API.new(config) } + + context "#initialize" do + before do + SiteSetting.auth0_client_id = "xyz" + SiteSetting.auth0_client_secret = "zyx" + SiteSetting.auth0_domain = "foobar.com" + end + + it "uses config options" do + expect(api.instance_variable_get(:@client_id)).to eq config[:client_id] + expect(api.instance_variable_get(:@client_secret)).to eq config[:client_secret] + expect(api.instance_variable_get(:@token_endpoint)).to eq config[:token_endpoint] + expect(api.instance_variable_get(:@url)).to eq config[:url] + expect(api.instance_variable_get(:@aud)).to eq config[:aud] + end + + it "uses default options if none are set" do + config = { + url: "https://example.com/api", + aud: "example.com" + } + api = MozillaIAM::API.new(config) + + expect(api.instance_variable_get(:@client_id)).to eq SiteSetting.auth0_client_id + expect(api.instance_variable_get(:@client_secret)).to eq SiteSetting.auth0_client_secret + expect(api.instance_variable_get(:@token_endpoint)).to eq "https://#{SiteSetting.auth0_domain}/oauth/token" + expect(api.instance_variable_get(:@url)).to eq config[:url] + expect(api.instance_variable_get(:@aud)).to eq config[:aud] + end + + it "throws error if url isn't specified" do + config = { aud: "example.com" } + expect { MozillaIAM::API.new(config) }.to raise_error "no url in config" + end + + it "throws error if aud isn't specified" do + config = { url: "https://example.com/api" } + expect { MozillaIAM::API.new(config) }.to raise_error "no aud in config" + end + end + + context "#get" do + let(:res_success) { return { status: 200, body: '{"success":"true"}' } } + + before do + api.expects(:access_token).returns('supersecret') + end + + it "should get the right path" do + stub_request(:get, "https://example.com/api/right_path").to_return(res_success) + expect(api.send(:get, "right_path")[:success]).to eq "true" + end + + it "should set the Authorization header" do + stub_request(:get, "https://example.com/api/").with(headers: { + "Authorization": "Bearer supersecret" + }).to_return(res_success) + expect(api.send(:get, "")[:success]).to eq "true" + end + + it "should send parameters" do + stub_request(:get, "https://example.com/api/?foo=bar").to_return(res_success) + expect(api.send(:get, "", foo: 'bar')[:success]).to eq "true" + end + + it "should return an empty hash if the the status code isn't 200" do + stub_request(:get, "https://example.com/api/").to_return(status: 403) + expect(api.send(:get, "")).to eq({}) + end + end + + context "#access_token" do + before do + api.stubs(:refresh_token).returns("refreshed_secret") + end + + it "fetches the token from aud prefix" do + ::PluginStore.set('mozilla-iam', "example.com_token", exp: Time.now.to_i + 1000, access_token: "secret") + expect(api.send(:access_token)).to eq "secret" + end + + it "refreshes the token if there's no saved token" do + expect(api.send(:access_token)).to eq "refreshed_secret" + end + + it "refreshes the token if the saved token is expired" do + ::PluginStore.set('mozilla-iam', "example.com_token", exp: Time.now.to_i, access_token: "secret") + expect(api.send(:access_token)).to eq "refreshed_secret" + end + end + + context "#refresh_token" do + it "stores token with aud prefix" do + api.expects(:fetch_token).returns("token") + api.expects(:verify_token).with("token").returns({ "exp" => "exp" }) + token = api.send(:refresh_token) + saved_token = ::PluginStore.get('mozilla-iam', "example.com_token") + expect(token).to eq "token" + expect(saved_token[:access_token]).to eq "token" + expect(saved_token[:exp]).to eq "exp" + end + end + + context "#fetch_token" do + it "fetches token from token_endpoint" do + stub_request(:post, "https://example.com/oauth/token").with( + body: { + grant_type: "client_credentials", + client_id: "abc", + client_secret: "def", + audience: "example.com" + } + ).to_return(status: 200, body: '{"access_token":"fetched_token"}') + token = api.send(:fetch_token) + expect(token).to eq "fetched_token" + end + end + + context "#verify_token" do + it "returns verified token" do + SiteSetting.auth0_domain = "example.com" + MozillaIAM::JWKS.expects(:public_key).with("jwt").returns("public_key") + ::JWT.expects(:decode).with("jwt", "public_key", true, { + algorithm: "RS256", + iss: "https://example.com/", + aud: "example.com", + sub: "abc@clients", + verify_iss: true, + verify_iat: true, + verify_aud: true, + verify_sub: true, + verify_iss: true + }).returns(["verified_token", "header"]) + token = api.send(:verify_token, "jwt") + expect(token).to eq "verified_token" + end + end +end diff --git a/spec/components/mozilla_iam/management_api_spec.rb b/spec/components/mozilla_iam/management_api_spec.rb new file mode 100644 index 0000000..fc9a2f8 --- /dev/null +++ b/spec/components/mozilla_iam/management_api_spec.rb @@ -0,0 +1,27 @@ +require_relative "../../iam_helper" + +describe MozillaIAM::ManagementAPI do + let(:api) { MozillaIAM::ManagementAPI.new } + before do + SiteSetting.auth0_domain = "foobar.com" + end + + context "#initialize" do + it "sets url and aud based on auth0_domain" do + expect(api.instance_variable_get(:@url)).to eq "https://foobar.com/api/v2" + expect(api.instance_variable_get(:@aud)).to eq "https://foobar.com/api/v2/" + end + end + + context "#profile" do + it "returns the management api profile" do + api.expects(:get).with("users/uid").returns("profile") + expect(api.profile("uid")).to eq "profile" + end + + it "returns an empty hash if a profile doesn't exist" do + api.expects(:get).with("users/uid").returns({}) + expect(api.profile("uid")).to eq({}) + end + end +end diff --git a/spec/components/mozilla_iam/person_api_spec.rb b/spec/components/mozilla_iam/person_api_spec.rb new file mode 100644 index 0000000..b6124cb --- /dev/null +++ b/spec/components/mozilla_iam/person_api_spec.rb @@ -0,0 +1,28 @@ +require_relative "../../iam_helper" + +describe MozillaIAM::PersonAPI do + let(:api) { MozillaIAM::PersonAPI.new } + before do + SiteSetting.mozilla_iam_person_api_url = "https://person.com" + SiteSetting.mozilla_iam_person_api_aud = "person.com" + end + + context "#initialize" do + it "sets url and aud based on SiteSetting" do + expect(api.instance_variable_get(:@url)).to eq "https://person.com" + expect(api.instance_variable_get(:@aud)).to eq "person.com" + end + end + + context "#profile" do + it "returns the profile for a specific user" do + api.expects(:get).with("profile/uid").returns(body: '{"profile":"profile"}') + expect(api.profile("uid")[:profile]).to eq "profile" + end + + it "returns an empty hash if a profile doesn't exist" do + api.expects(:get).with("profile/uid").returns(body: '{}') + expect(api.profile("uid")).to eq({}) + end + end +end diff --git a/spec/components/mozilla_iam/profile_spec.rb b/spec/components/mozilla_iam/profile_spec.rb index a63ae28..e391dcb 100644 --- a/spec/components/mozilla_iam/profile_spec.rb +++ b/spec/components/mozilla_iam/profile_spec.rb @@ -1,9 +1,19 @@ require_relative '../../iam_helper' describe MozillaIAM::Profile do + let(:user) { Fabricate(:user) } + let(:profile) { MozillaIAM::Profile.new(user, "uid") } + context '.refresh' do + it "refreshes a user who already has a profile" do + profile + MozillaIAM::Profile.expects(:new).with(user, "uid").returns(profile) + MozillaIAM::Profile.any_instance.expects(:refresh).returns(true) + result = MozillaIAM::Profile.refresh(user) + expect(result).to be true + end + it 'should return nil if user has no profile' do - user = Fabricate(:user) result = MozillaIAM::Profile.refresh(user) expect(result).to be_nil end @@ -11,23 +21,107 @@ describe MozillaIAM::Profile do context '#initialize' do it "should save a user's uid" do - user = Fabricate(:user) - uid = create_uid(user.username) - - MozillaIAM::Profile.new(user, uid) - - expect(user.custom_fields['mozilla_iam_uid']).to eq(uid) + profile + expect(user.custom_fields['mozilla_iam_uid']).to eq("uid") end end context '#refresh' do - it "should refresh a user's profile if it hasn't been refreshed before" do - user = Fabricate(:user) - uid = create_uid(user.username) + it "returns #force_refresh if #should_refresh? is true" do + profile.expects(:should_refresh?).returns(true) + profile.expects(:last_refresh).never + profile.expects(:force_refresh).once.returns(true) + expect(profile.refresh).to be true + end - result = MozillaIAM::Profile.new(user, uid).refresh + it "returns #last_refresh if #should_refresh? is false" do + profile.expects(:should_refresh?).returns(false) + profile.expects(:force_refresh).never + profile.expects(:last_refresh).once.returns(true) + expect(profile.refresh).to be true + end + end - expect(result).to be_within(5.seconds).of Time.now + context "#force_refresh" do + it "calls update_groups" do + profile.expects(:update_groups) + profile.force_refresh + end + + it "sets the last refresh to now and returns it" do + profile.expects(:set_last_refresh).with() { |t| t.between?(Time.now() - 5, Time.now()) }.returns("time now") + expect(profile.force_refresh).to eq "time now" + end + end + + context "#groups" do + it "merges app_metadata.groups into groups from management api" do + profile.expects(:mgmt_profile).at_least_once.returns(groups: ['a'], app_metadata: { groups: ['b', 'a', 'c'] }) + expect(profile.groups).to eq ['a', 'b', 'c'] + end + + it "returns app_metadata.groups when groups is nil" do + profile.expects(:mgmt_profile).at_least_once.returns(app_metadata: { groups: ['a', 'b', 'c'] }) + expect(profile.groups).to eq ['a', 'b', 'c'] + end + + it "returns groups when app_metadata is nil" do + profile.expects(:mgmt_profile).at_least_once.returns(groups: ['a', 'b', 'c']) + expect(profile.groups).to eq ['a', 'b', 'c'] + end + + it "returns empty array when groups and app_metadata are nil" do + profile.expects(:mgmt_profile).at_least_once.returns({}) + expect(profile.groups).to eq [] + end + end + + context "#mgmt_profile" do + it "returns a user's profile from the Management API and stores it in an instance variable" do + MozillaIAM::ManagementAPI.any_instance.expects(:profile).with("uid").returns("profile") + expect(profile.send(:mgmt_profile)).to eq "profile" + expect(profile.instance_variable_get(:@mgmt_profile)).to eq "profile" + end + end + + context "#last_refresh" do + it "returns a user's last refreshed time if set and stores it in an instance variable" do + time_string = Time.now().to_s + time = Time.parse(time_string) + profile.expects(:get).returns(time_string) + expect(profile.send(:last_refresh)).to eq time + expect(profile.instance_variable_get(:@last_refresh)).to eq time + end + + it "returns nil if a user's has no last refresh time" do + profile.expects(:get).returns(nil) + expect(profile.send(:last_refresh)).to be_nil + end + end + + context "#set_last_refresh" do + it "stores a time and stores it in an instance variable" do + time = Time.now() + profile.expects(:set).with(:last_refresh, time).returns(time) + expect(profile.send(:set_last_refresh, time)).to eq time + expect(profile.instance_variable_get(:@last_refresh)).to eq time + end + end + + context "#should_refresh?" do + it "returns true if last_refresh is nil" do + profile.expects(:last_refresh).returns(nil) + expect(profile.send(:should_refresh?)).to be true + end + + it "returns true if last_refresh was over 15 minutes ago" do + profile.expects(:last_refresh).at_least_once.returns(Time.now() - 16.minutes) + expect(profile.send(:should_refresh?)).to be true + end + + it "returns false if last_refresh was within 15 minutes ago" do + profile.expects(:last_refresh).at_least_once.returns(Time.now() - 14.minutes) + expect(profile.send(:should_refresh?)).to be false end end @@ -38,55 +132,106 @@ describe MozillaIAM::Profile do before do MozillaIAM::GroupMapping.new(iam_group_name: 'iam_group', - authoritative: false, group: group).save! end it 'should remove a user from a mapped group' do + profile.expects(:groups).returns([]) group.users << user expect(group.users.count).to eq 1 - stub_api_users_request(uid, groups: []) + profile.send(:update_groups) - MozillaIAM::Profile.new(user, uid).refresh expect(group.users.count).to eq 0 end it 'should add a user to a mapped group' do + profile.expects(:groups).returns(['iam_group']) expect(group.users.count).to eq 0 - stub_api_users_request(uid, groups: ['iam_group']) + profile.send(:update_groups) - MozillaIAM::Profile.new(user, uid).refresh expect(group.users.count).to eq 1 end it 'should work if groups attribute is undefined' do expect(group.users.count).to eq 0 - stub_api_users_request(uid, {}) + MozillaIAM::ManagementAPI.any_instance.expects(:profile).returns(groups: nil, app_metadata: { groups: nil }) - MozillaIAM::Profile.new(user, uid).refresh + profile.send(:update_groups) expect(group.users.count).to eq 0 end it 'should work if groups attribute is an empty string' do expect(group.users.count).to eq 0 - stub_api_users_request(uid, groups: '') + MozillaIAM::ManagementAPI.any_instance.expects(:profile).returns(groups: '', app_metadata: { groups: '' }) - MozillaIAM::Profile.new(user, uid).refresh + profile.send(:update_groups) expect(group.users.count).to eq 0 end it 'should work if groups attribute is "None"' do expect(group.users.count).to eq 0 - stub_api_users_request(uid, groups: 'None') + MozillaIAM::ManagementAPI.any_instance.expects(:profile).returns(groups: "None", app_metadata: { groups: "None" }) - MozillaIAM::Profile.new(user, uid).refresh + profile.send(:update_groups) expect(group.users.count).to eq 0 end end + + context "#add_to_group" do + it "adds the user to a group" do + group = Fabricate(:group) + profile.send(:add_to_group, group) + expect(group.users.first).to eq user + end + end + + context "#remove_from_group" do + it "removes the user from a group" do + group = Fabricate(:group, users: [user]) + profile.send(:remove_from_group, group) + expect(group.users.count).to eq 0 + end + + it "doesn't error out when removing a user from a group they're not in" do + group = Fabricate(:group) + profile.send(:remove_from_group, group) + expect(group.users.count).to eq 0 + end + end + + context ".get" do + it "returns a value for the key" do + described_class.set(user, "key", "value") + expect(described_class.get(user, "key")).to eq "value" + end + end + + context "#get" do + it "calls .get with the user" do + described_class.expects(:get).with(user, "key").returns("value") + expect(profile.send(:get, "key")).to eq "value" + end + end + + context ".set" do + it "saves the value for a key and returns it" do + value = described_class.set(user, "key", "value") + expect(value).to eq "value" + expect(described_class.get(user, "key")).to eq "value" + end + end + + context "#set" do + it "calls .set with the user" do + profile + described_class.expects(:set).with(user, "key", "value").returns("value") + expect(profile.send(:set, "key", "value")).to eq "value" + end + end end diff --git a/spec/models/group_mapping_spec.rb b/spec/models/group_mapping_spec.rb index 1c40b79..c11925c 100644 --- a/spec/models/group_mapping_spec.rb +++ b/spec/models/group_mapping_spec.rb @@ -3,9 +3,7 @@ require_relative '../iam_helper' describe MozillaIAM::GroupMapping do it 'should be destroyed when associated group is destroyed' do group = Fabricate(:group) - mapping = MozillaIAM::GroupMapping.new(iam_group_name: 'iam_group', - authoritative: false, - group: group) + mapping = MozillaIAM::GroupMapping.new(iam_group_name: 'iam_group', group: group) mapping.save! mapping.reload diff --git a/spec/mozilla_iam_spec.rb b/spec/mozilla_iam_spec.rb index 073c5c1..9982edc 100644 --- a/spec/mozilla_iam_spec.rb +++ b/spec/mozilla_iam_spec.rb @@ -15,7 +15,6 @@ describe MozillaIAM do before do MozillaIAM::GroupMapping.new(iam_group_name: 'iam_group', - authoritative: false, group: group).save! TopicUser.change(user.id, topic.id, notification_level: TopicUser.notification_levels[:watching]) user.custom_fields['mozilla_iam_uid'] = uid @@ -24,7 +23,7 @@ describe MozillaIAM do end context 'when user in correct IAM group' do - before { stub_api_users_request(uid, groups: ['iam_group']) } + before { stub_management_api_profile_request(uid, groups: ['iam_group']) } it 'refreshes the user profile' do PostAlerter.post_created(reply) @@ -52,7 +51,7 @@ describe MozillaIAM do end context 'when user removed from IAM group' do - before { stub_api_users_request(uid, groups: []) } + before { stub_management_api_profile_request(uid, groups: []) } it 'refreshes the user profile' do PostAlerter.post_created(reply) diff --git a/spec/serializers/admin_detailed_user_serializer_spec.rb b/spec/serializers/admin_detailed_user_serializer_spec.rb new file mode 100644 index 0000000..430cbc8 --- /dev/null +++ b/spec/serializers/admin_detailed_user_serializer_spec.rb @@ -0,0 +1,28 @@ +require_relative '../iam_helper' + +describe AdminDetailedUserSerializer do + let(:user) { Fabricate(:user) } + let(:json) { AdminDetailedUserSerializer.new(user, scope: Guardian.new, root:false).as_json } + + describe "#mozilla_iam" do + it "should contain 'mozilla_iam' prefixed custom fields" do + mozilla_iam_one = 'Some IAM data' + mozilla_iam_two = 'Some more IAM data' + + user.custom_fields['mozilla_iam_one'] = mozilla_iam_one + user.custom_fields['mozilla_iam_two'] = mozilla_iam_two + user.save + + mozilla_iam = json[:mozilla_iam] + expect(mozilla_iam['one']).to eq(mozilla_iam_one) + expect(mozilla_iam['two']).to eq(mozilla_iam_two) + end + + it "shouldn't contain non-'mozilla_iam' prefixed custom fields" do + user.custom_fields['other_custom_fields'] = 'some data' + user.save + + expect(json[:mozilla_iam]).to be_empty + end + end +end diff --git a/spec/support/iam_helpers.rb b/spec/support/iam_helpers.rb index da09c95..dd974ce 100644 --- a/spec/support/iam_helpers.rb +++ b/spec/support/iam_helpers.rb @@ -32,6 +32,10 @@ module IAMHelpers .to_return(status: 200, body: create_jwks) end + def create_jwt(payload, header) + JWT.encode(payload, private_key, 'RS256', header) + end + def create_id_token(user, additional_payload = {}, additional_header = {}) payload = { name: user[:name], @@ -48,7 +52,7 @@ module IAMHelpers kid: 'the_best_key' }.merge(additional_header) - JWT.encode(payload, private_key, 'RS256', header_fields) + create_jwt(payload, header_fields) end def create_uid(username) @@ -71,13 +75,13 @@ module IAMHelpers authenticate_with_id_token create_id_token(user) end - def stub_oauth_token_request + def stub_oauth_token_request(aud) stub_jwks_request payload = { sub: 'the_best_client_id@clients', iss: 'https://auth.mozilla.auth0.com/', - aud: 'https://auth.mozilla.auth0.com/api/v2/', + aud: aud, exp: Time.now.to_i + 7.days, iat: Time.now.to_i } @@ -93,10 +97,17 @@ module IAMHelpers .to_return(status: 200, body: body) end - def stub_api_users_request(uid, app_metadata) - stub_oauth_token_request + def stub_people_api_profile_request(uid, profile) + stub_oauth_token_request('https://person-api.sso.mozilla.com') - stub_request(:get, "https://auth.mozilla.auth0.com/api/v2/users/#{uid}?fields=app_metadata") - .to_return(status: 200, body: MultiJson.dump(app_metadata: app_metadata)) + stub_request(:get, "https://uhbz4h3wa8.execute-api.us-west-2.amazonaws.com/prod/profile/#{uid}") + .to_return(status: 200, body: MultiJson.dump(body: MultiJson.dump(profile))) + end + + def stub_management_api_profile_request(uid, profile) + stub_oauth_token_request('https://auth.mozilla.auth0.com/api/v2/') + + stub_request(:get, "https://auth.mozilla.auth0.com/api/v2/users/#{uid}") + .to_return(status: 200, body: MultiJson.dump(app_metadata: profile)) end end