diff --git a/auslib/db.py b/auslib/db.py index e07739b731..a22dc070b5 100644 --- a/auslib/db.py +++ b/auslib/db.py @@ -2295,10 +2295,16 @@ def assertOptionsExist(self, permission, options): raise ValueError('Unknown option "%s" for permission "%s"' % (opt, permission)) def getAllUsers(self, transaction=None): - res_permissions = self.select(columns=[self.username], distinct=True, transaction=transaction) - res_roles = self.user_roles.select(columns=[self.user_roles.username], transaction=transaction) - res = res_roles + res_permissions - return list(set([r['username'] for r in res])) + res_users = self.select(columns=[self.username], distinct=True, transaction=transaction) + users_list = list([r['username'] for r in res_users]) + users = {} + for user in users_list: + res_roles = self.user_roles.select(where=[ + self.user_roles.username == user], + columns=[self.user_roles.role, self.user_roles.data_version], + transaction=transaction) + users[user] = {"roles": res_roles} + return users def getAllPermissions(self, transaction=None): ret = defaultdict(dict) @@ -2423,10 +2429,6 @@ def getUserRoles(self, username, transaction=None): distinct=True, transaction=transaction) return [{"role": r["role"], "data_version": r["data_version"]} for r in res] - def getAllRoles(self, transaction=None): - res = self.user_roles.select(columns=[self.user_roles.role], distinct=True, transaction=transaction) - return [r["role"] for r in res] - def isAdmin(self, username, transaction=None): return bool(self.getPermission(username, "admin", transaction)) diff --git a/auslib/test/admin/views/test_permissions.py b/auslib/test/admin/views/test_permissions.py index 504df00bcb..99e2dccadd 100644 --- a/auslib/test/admin/views/test_permissions.py +++ b/auslib/test/admin/views/test_permissions.py @@ -11,8 +11,15 @@ def testUsers(self): ret = self._get('/users') self.assertEqual(ret.status_code, 200) data = json.loads(ret.data) - data['users'] = set(data['users']) - self.assertEqual(data, dict(users=set(['bill', 'billy', 'bob', 'ashanti', 'mary', 'julie']))) + self.assertEqual(data, ({ + 'ashanti': {'roles': []}, + 'bill': {'roles': [ + {'role': 'qa', 'data_version': 1}, + {'role': 'releng', 'data_version': 1}]}, + 'billy': {'roles': []}, + 'bob': {'roles': [{'role': 'relman', 'data_version': 1}]}, + 'julie': {'roles': [{'role': 'releng', 'data_version': 1}]}, + 'mary': {'roles': [{'role': 'relman', 'data_version': 1}]}})) class TestCurrentUserAPI_JSON(ViewTest): @@ -763,6 +770,51 @@ def testGetScheduledChangeHistoryRevisions(self): } self.assertEquals(json.loads(ret.data), expected) + def testGetScheduledChangeHistory(self): + ret = self._get("/permissions_scheduled_change/history") + self.assertEquals(ret.status_code, 200, ret.data) + expected = { + "count": 7, + "revisions": [ + { + "change_id": 13, "changed_by": "bill", "complete": False, "timestamp": 405, "sc_id": 6, "scheduled_by": "bill", + "change_type": "update", "data_version": 1, "permission": "release", "options": {"products": ["a", "b"]}, + "username": "bob", "when": 38000000, "sc_data_version": 1 + }, + { + "change_id": 11, "changed_by": "bill", "complete": False, "timestamp": 205, "sc_id": 5, "scheduled_by": "bill", + "change_type": "insert", "data_version": None, "permission": "rule", "username": "joe", "options": {"products": ["fake"]}, + "when": 98000000, "sc_data_version": 1 + }, + { + "change_id": 9, "changed_by": "bill", "timestamp": 201, "sc_id": 4, "scheduled_by": "bill", "change_type": "delete", "data_version": None, + "permission": "scheduled_change", "username": "mary", "complete": False, "options": None, "sc_data_version": 1, "when": 76000000 + }, + { + "change_id": 7, "changed_by": "bill", "timestamp": 100, "sc_id": 3, "scheduled_by": "bill", "change_type": "insert", + "data_version": None, "permission": "permission", "username": "bob", "options": None, "when": 30000000, "complete": True, + "sc_data_version": 2, + }, + { + "change_id": 6, "changed_by": "bill", "timestamp": 61, "sc_id": 3, "scheduled_by": "bill", "change_type": "insert", + "data_version": None, "permission": "permission", "username": "bob", "options": None, "when": 30000000, "complete": False, + "sc_data_version": 1, + }, + { + "change_id": 4, "changed_by": "bill", "complete": False, "timestamp": 41, "sc_id": 2, "scheduled_by": "bill", + "change_type": "update", "data_version": 1, "permission": "release_locale", "username": "ashanti", "options": None, + "sc_data_version": 1, "when": 20000000 + }, + { + "change_id": 2, "changed_by": "bill", "complete": False, "timestamp": 21, "sc_id": 1, "scheduled_by": "bill", + "change_type": "insert", "data_version": None, "permission": "rule", "username": "janet", "options": {"products": ["foo"]}, + "sc_data_version": 1, "when": 10000000 + }, + ], + } + + self.assertEquals(json.loads(ret.data), expected) + @mock.patch("time.time", mock.MagicMock(return_value=100)) def testSignoffWithPermission(self): ret = self._post("/scheduled_changes/permissions/2/signoffs", data=dict(role="relman"), username="bob") @@ -848,23 +900,6 @@ def testAddNewFullAdminPermission(self): class TestUserRolesAPI_JSON(ViewTest): - def testGetRoles(self): - ret = self._get("/users/bill/roles") - self.assertStatusCode(ret, 200) - got = json.loads(ret.data)["roles"] - self.assertEquals(got, [{"role": "qa", "data_version": 1}, - {"role": "releng", "data_version": 1}]) - - def testGetAllRoles(self): - ret = self._get("/users/roles") - self.assertStatusCode(ret, 200) - got = json.loads(ret.data)["roles"] - self.assertEqual(got, ['releng', 'qa', 'relman']) - - def testGetRolesMissingUserReturnsEmptyList(self): - ret = self.client.get("/users/dean/roles") - self.assertStatusCode(ret, 200) - def testGrantRole(self): ret = self._put("/users/ashanti/roles/dev") self.assertStatusCode(ret, 201) diff --git a/auslib/test/admin/views/test_releases.py b/auslib/test/admin/views/test_releases.py index 89759da219..96cb276687 100644 --- a/auslib/test/admin/views/test_releases.py +++ b/auslib/test/admin/views/test_releases.py @@ -1666,6 +1666,42 @@ def testGetScheduledChangeHistoryRevisions(self): } self.assertEquals(ret, expected) + def testGetScheduledChangeHistory(self): + ret = self._get("/releases_scheduled_change/history") + self.assertEquals(ret.status_code, 200, ret.data) + ret = json.loads(ret.data) + expected = { + 'count': 5, + 'revisions': [ + { + 'product': 'a', 'changed_by': 'bill', 'data': {'name': 'a', 'extv': '2.0', 'hashFunction': 'sha512', 'schema_version': 1}, + 'sc_id': 2, 'sc_data_version': 1, 'scheduled_by': 'bill', 'data_version': 1, 'complete': False, 'when': 6000000000, + 'change_type': 'update', 'name': 'a', 'timestamp': 71, 'change_id': 4, 'read_only': False + }, + { + 'product': 'm', 'changed_by': 'bill', 'data': {'name': 'm', 'hashFunction': 'sha512', 'schema_version': 1}, + 'sc_id': 1, 'sc_data_version': 1, 'scheduled_by': 'bill', 'data_version': None, 'complete': False, + 'when': 4000000000, 'change_type': 'insert', 'name': 'm', 'timestamp': 51, 'change_id': 2, 'read_only': False + }, + { + 'product': None, 'changed_by': 'bill', 'data': None, 'sc_id': 4, 'sc_data_version': 1, 'scheduled_by': 'bill', + 'data_version': 1, 'complete': False, 'when': 230000000, 'change_type': 'delete', 'name': 'ab', 'timestamp': 26, + 'change_id': 9, 'read_only': False + }, + { + 'product': 'b', 'changed_by': 'bill', 'data': {'name': 'b', 'hashFunction': 'sha512', 'schema_version': 1}, + 'sc_id': 3, 'sc_data_version': 2, 'scheduled_by': 'bill', 'data_version': 1, 'complete': True, 'when': 10000000, + 'change_type': 'update', 'name': 'b', 'timestamp': 25, 'change_id': 7, 'read_only': False + }, + { + 'product': 'b', 'changed_by': 'bill', 'data': {'name': 'b', 'hashFunction': 'sha512', 'schema_version': 1}, + 'sc_id': 3, 'sc_data_version': 1, 'scheduled_by': 'bill', 'data_version': 1, 'complete': False, + 'when': 10000000, 'change_type': 'update', 'name': 'b', 'timestamp': 7, 'change_id': 6, 'read_only': False + }, + ], + } + self.assertEquals(ret, expected) + @mock.patch("time.time", mock.MagicMock(return_value=100)) def testSignoffWithPermission(self): ret = self._post("/scheduled_changes/releases/1/signoffs", data=dict(role="qa"), username="bill") @@ -1729,6 +1765,71 @@ def testGetRevisions(self): with self.assertRaises(KeyError): data['data'] + def testGetHistory(self): + url = '/releases/history' + ret = self._get(url) + self.assertEquals(ret.status_code, 200, msg=ret.data) + data = json.loads(ret.data) + self.assertEquals(data["count"], 3) + expected = { + 'count': 3, + 'revisions': [ + { + 'product': 'b', 'data_version': 1, 'changed_by': 'bill', '_different': [], 'read_only': 'False', + 'change_id': 6, '_time_ago': '48 years and 583 months ago', 'name': 'b', 'timestamp': 16 + }, + { + 'product': 'd', 'data_version': 1, 'changed_by': 'bill', '_different': ['product', 'name', 'data'], + 'read_only': 'False', 'change_id': 4, '_time_ago': '48 years and 583 months ago', 'name': 'd', + 'timestamp': 10 + }, + { + 'product': 'a', 'data_version': 1, 'changed_by': 'bill', '_different': ['product', 'name', 'data'], + 'read_only': 'False', 'change_id': 2, '_time_ago': '48 years and 583 months ago', 'name': 'ab', + 'timestamp': 6 + }, + ], + } + revisions = data["revisions"] + expected_revisions = expected["revisions"] + for index in range(len(revisions)): + self.assertEquals(revisions[index]['product'], expected_revisions[index]['product']) + self.assertEquals(revisions[index]['timestamp'], expected_revisions[index]['timestamp']) + self.assertEquals(revisions[index]['read_only'], expected_revisions[index]['read_only']) + self.assertEquals(revisions[index]['data_version'], expected_revisions[index]['data_version']) + self.assertEquals(revisions[index]['changed_by'], expected_revisions[index]['changed_by']) + self.assertEquals(len(data["revisions"]), 3) + + def testGetHistoryWithTimeRange(self): + url = '/releases/history' + ret = self._get(url, qs={"timestamp_from": 8}) + self.assertEquals(ret.status_code, 200, msg=ret.data) + data = json.loads(ret.data) + self.assertEquals(data["count"], 2) + expected = { + 'count': 2, + 'revisions': [ + { + 'product': 'b', 'data_version': 1, 'changed_by': 'bill', '_different': [], 'read_only': 'False', + 'change_id': 6, '_time_ago': '48 years and 583 months ago', 'name': 'b', 'timestamp': 16 + }, + { + 'product': 'd', 'data_version': 1, 'changed_by': 'bill', '_different': ['product', 'name', 'data'], + 'read_only': 'False', 'change_id': 4, '_time_ago': '48 years and 583 months ago', 'name': 'd', + 'timestamp': 10 + }, + ], + } + revisions = data["revisions"] + expected_revisions = expected["revisions"] + for index in range(len(revisions)): + self.assertEquals(revisions[index]['product'], expected_revisions[index]['product']) + self.assertEquals(revisions[index]['timestamp'], expected_revisions[index]['timestamp']) + self.assertEquals(revisions[index]['read_only'], expected_revisions[index]['read_only']) + self.assertEquals(revisions[index]['data_version'], expected_revisions[index]['data_version']) + self.assertEquals(revisions[index]['changed_by'], expected_revisions[index]['changed_by']) + self.assertEquals(len(data["revisions"]), 2) + def testPostRevisionRollback(self): # Make some changes to a release data = json.dumps(dict(detailsUrl='beep', fakePartials=True, schema_version=1)) diff --git a/auslib/test/admin/views/test_required_signoffs.py b/auslib/test/admin/views/test_required_signoffs.py index 1608ad04b0..6c319f40d9 100644 --- a/auslib/test/admin/views/test_required_signoffs.py +++ b/auslib/test/admin/views/test_required_signoffs.py @@ -88,6 +88,56 @@ def testGetRevisions(self): self.assertEquals(got["count"], 2) self.assertEquals(got["required_signoffs"], expected) + def testGetAllHistory(self): + ret = self._get("/required_signoffs/product/history", qs={}) + self.assertStatusCode(ret, 200) + + got = json.loads(ret.data) + expected = [ + { + "change_id": 3, + "changed_by": "bill", + "timestamp": 25, + "product": "fake", + "channel": "k", + "role": "relman", + "signoffs_required": 1, + "data_version": 2, + }, + { + "change_id": 2, + "changed_by": "bill", + "timestamp": 11, + "product": "fake", + "channel": "k", + "role": "relman", + "signoffs_required": 2, + "data_version": 1, + }, + ] + self.assertEquals(got["count"], 2) + self.assertEquals(got["required_signoffs"], expected) + + def testGetAllHistoryWithinTimeRange(self): + ret = self._get("/required_signoffs/product/history", qs={"timestamp_from": 20, "timestamp_to": 30}) + self.assertStatusCode(ret, 200) + + got = json.loads(ret.data) + expected = [ + { + "change_id": 3, + "changed_by": "bill", + "timestamp": 25, + "product": "fake", + "channel": "k", + "role": "relman", + "signoffs_required": 1, + "data_version": 2, + }, + ] + self.assertEquals(got["count"], 1) + self.assertEquals(got["required_signoffs"], expected) + class TestProductRequiredSignoffsScheduledChanges(ViewTest): maxDiff = 10000 @@ -449,6 +499,84 @@ def testGetScheduledChangeHistoryRevisions(self): } self.assertEquals(json.loads(ret.data), expected) + def testGetAllScheduledChangeHistory(self): + ret = self._get("/product_required_signoffs_scheduled_change/history") + self.assertEquals(ret.status_code, 200, ret.data) + expected = { + "count": 5, + "revisions": [ + { + "changed_by": "bill", "timestamp": 201, "sc_id": 4, "product": "fake", "data_version": 1, "change_type": "delete", + "complete": False, "change_id": 9, "channel": "j", "sc_data_version": 1, "when": 400000000, "signoffs_required": None, + "role": "releng", "scheduled_by": "bill" + }, + { + "changed_by": "bill", "timestamp": 100, "sc_id": 3, "product": "fake", "data_version": None, "change_type": "insert", + "complete": False, "change_id": 7, "channel": "e", "sc_data_version": 2, "when": 300000000, "signoffs_required": 1, + "role": "releng", "scheduled_by": "bill" + }, + { + "changed_by": "bill", "timestamp": 81, "sc_id": 3, "product": "fake", "data_version": None, "change_type": "insert", + "complete": False, "change_id": 6, "channel": "e", "sc_data_version": 1, "when": 300000000, "signoffs_required": 2, + "role": "releng", "scheduled_by": "bill" + }, + { + "changed_by": "bill", "timestamp": 41, "sc_id": 2, "product": "fake", "data_version": 1, "change_type": "update", + "complete": False, "change_id": 4, "channel": "a", "sc_data_version": 1, "when": 200000000, "signoffs_required": 2, + "role": "releng", "scheduled_by": "bill" + }, + { + "changed_by": "bill", "timestamp": 21, "sc_id": 1, "product": "fake", "data_version": None, "change_type": "insert", + "complete": False, "change_id": 2, "channel": "a", "sc_data_version": 1, "when": 100000000, "signoffs_required": 1, + "role": "relman", "scheduled_by": "bill" + }, + ], + } + data = json.loads(ret.data) + revisions = data["revisions"] + expected_revisions = expected["revisions"] + for index in range(len(revisions)): + self.assertEquals(revisions[index]['product'], expected_revisions[index]['product']) + self.assertEquals(revisions[index]['scheduled_by'], expected_revisions[index]['scheduled_by']) + self.assertEquals(revisions[index]['change_id'], expected_revisions[index]['change_id']) + self.assertEquals(revisions[index]['data_version'], expected_revisions[index]['data_version']) + self.assertEquals(revisions[index]['changed_by'], expected_revisions[index]['changed_by']) + self.assertEquals(len(data["revisions"]), 5) + + def testGetAllScheduledChangeHistoryWithinTimeRange(self): + ret = self._get("/product_required_signoffs_scheduled_change/history", qs={"timestamp_from": 41, "timestamp_to": 100}) + self.assertEquals(ret.status_code, 200, ret.data) + expected = { + "count": 3, + "revisions": [ + { + "changed_by": "bill", "timestamp": 100, "sc_id": 3, "product": "fake", "data_version": None, "change_type": "insert", + "complete": False, "change_id": 7, "channel": "e", "sc_data_version": 2, "when": 300000000, "signoffs_required": 1, + "role": "releng", "scheduled_by": "bill" + }, + { + "changed_by": "bill", "timestamp": 81, "sc_id": 3, "product": "fake", "data_version": None, "change_type": "insert", + "complete": False, "change_id": 6, "channel": "e", "sc_data_version": 1, "when": 300000000, "signoffs_required": 2, + "role": "releng", "scheduled_by": "bill" + }, + { + "changed_by": "bill", "timestamp": 41, "sc_id": 2, "product": "fake", "data_version": 1, "change_type": "update", + "complete": False, "change_id": 4, "channel": "a", "sc_data_version": 1, "when": 200000000, "signoffs_required": 2, + "role": "releng", "scheduled_by": "bill" + }, + ], + } + data = json.loads(ret.data) + revisions = data["revisions"] + expected_revisions = expected["revisions"] + for index in range(len(revisions)): + self.assertEquals(revisions[index]['product'], expected_revisions[index]['product']) + self.assertEquals(revisions[index]['scheduled_by'], expected_revisions[index]['scheduled_by']) + self.assertEquals(revisions[index]['change_id'], expected_revisions[index]['change_id']) + self.assertEquals(revisions[index]['data_version'], expected_revisions[index]['data_version']) + self.assertEquals(revisions[index]['changed_by'], expected_revisions[index]['changed_by']) + self.assertEquals(len(data["revisions"]), 3) + @mock.patch("time.time", mock.MagicMock(return_value=100)) def testSignoffWithPermission(self): ret = self._post("/scheduled_changes/required_signoffs/product/2/signoffs", data=dict(role="relman"), username="bob") @@ -559,6 +687,53 @@ def testGetRevisions(self): self.assertEquals(got["count"], 2) self.assertEquals(got["required_signoffs"], expected) + def testGetAllHistory(self): + ret = self._get("/required_signoffs/permissions/history", qs={}) + self.assertStatusCode(ret, 200) + + got = json.loads(ret.data) + expected = [ + { + "change_id": 3, + "changed_by": "bill", + "timestamp": 25, + "product": "doop", + "role": "releng", + "signoffs_required": 1, + "data_version": 2, + }, + { + "change_id": 2, + "changed_by": "bill", + "timestamp": 11, + "product": "doop", + "role": "releng", + "signoffs_required": 2, + "data_version": 1, + }, + ] + self.assertEquals(got["count"], 2) + self.assertEquals(got["required_signoffs"], expected) + + def testGetAllHistoryWithinTimeRange(self): + ret = self._get("/required_signoffs/permissions/history", qs={"timestamp_from": 20, "timestamp_to": 30}) + self.assertStatusCode(ret, 200) + + got = json.loads(ret.data) + expected = [ + { + "change_id": 3, + "changed_by": "bill", + "timestamp": 25, + "product": "doop", + "role": "releng", + "signoffs_required": 1, + "data_version": 2, + }, + ] + self.assertEquals(got["count"], 1) + self.assertEquals(got["required_signoffs"], expected) + class TestPermissionsRequiredSignoffsScheduledChanges(ViewTest): maxDiff = 10000 @@ -914,6 +1089,33 @@ def testGetScheduledChangeHistoryRevisions(self): } self.assertEquals(json.loads(ret.data), expected) + def testGetScheduledChangeHistory(self): + ret = self._get("/required_signoffs/permissions/history") + self.assertEquals(ret.status_code, 200, ret.data) + expected = { + "count": 2, + "required_signoffs": [ + { + "data_version": 2, "changed_by": "bill", "product": "doop", "change_id": 3, "role": "releng", + "signoffs_required": 1, "timestamp": 25}, + { + "data_version": 1, "changed_by": "bill", "product": "doop", "change_id": 2, "role": "releng", + "signoffs_required": 2, "timestamp": 11 + }, + ], + } + data = json.loads(ret.data) + revisions = data["required_signoffs"] + expected_revisions = expected["required_signoffs"] + for index in range(len(revisions)): + self.assertEquals(revisions[index]['product'], expected_revisions[index]['product']) + self.assertEquals(revisions[index]['timestamp'], expected_revisions[index]['timestamp']) + self.assertEquals(revisions[index]['change_id'], expected_revisions[index]['change_id']) + self.assertEquals(revisions[index]['data_version'], expected_revisions[index]['data_version']) + self.assertEquals(revisions[index]['changed_by'], expected_revisions[index]['changed_by']) + self.assertEquals(len(data["required_signoffs"]), 2) + self.assertEquals(json.loads(ret.data), expected) + @mock.patch("time.time", mock.MagicMock(return_value=100)) def testSignoffWithPermission(self): ret = self._post("/scheduled_changes/required_signoffs/permissions/2/signoffs", data=dict(role="relman"), username="bob") diff --git a/auslib/test/admin/views/test_rules.py b/auslib/test/admin/views/test_rules.py index 3665faf6a5..68d9a11bbb 100644 --- a/auslib/test/admin/views/test_rules.py +++ b/auslib/test/admin/views/test_rules.py @@ -767,6 +767,53 @@ def testGetRevisions(self): self.assertTrue(u"rule_id" in got["rules"][0]) self.assertTrue(u"backgroundRate" in got["rules"][0]) + def testGetHistory(self): + # Make some changes to a rule + ret = self._post( + '/rules/1', + data=dict( + backgroundRate=71, + mapping='d', + priority=73, + data_version=1, + product='Firefox', + update_type='minor', + channel='nightly', + ) + ) + self.assertEquals( + ret.status_code, + 200, + "Status Code: %d, Data: %s" % (ret.status_code, ret.data) + ) + # and again + ret = self._post( + '/rules/1', + data=dict( + backgroundRate=72, + mapping='d', + priority=73, + data_version=2, + product='Firefux', + update_type='minor', + channel='nightly', + ) + ) + self.assertEquals( + ret.status_code, + 200, + "Status Code: %d, Data: %s" % (ret.status_code, ret.data) + ) + + url = '/rules/history' + ret = self._get(url) + got = json.loads(ret.data) + self.assertEquals(ret.status_code, 200, msg=ret.data) + self.assertEquals(got["count"], 2) + self.assertTrue(u"rule_id" in got["revisions"][0]) + self.assertTrue(u"backgroundRate" in got["revisions"][0]) + self.assertTrue(u"timestamp" in got["revisions"][0]) + def testVersionMaxFieldLength(self): # Max field length of rules.version is 75 version = '3.3,3.4,3.5,3.6,3.8,3.9,3.10,3.11' @@ -1604,6 +1651,16 @@ def testGetScheduledChangeHistoryRevisions(self): } self.assertEquals(json.loads(ret.data), expected) + def testGetAllScheduledChangeHistory(self): + ret = self._get("/rules_scheduled_change/history") + got = json.loads(ret.data) + self.assertEquals(ret.status_code, 200) + self.assertEquals(ret.status_code, 200, msg=ret.data) + self.assertEquals(got["count"], 8) + self.assertTrue(u"rule_id" in got["revisions"][0]) + self.assertTrue(u"backgroundRate" in got["revisions"][0]) + self.assertTrue(u"timestamp" in got["revisions"][0]) + @mock.patch("time.time", mock.MagicMock(return_value=300)) def testRevertScheduledChange(self): ret = self._post("/scheduled_changes/rules/3/revisions", data={"change_id": 2}) diff --git a/auslib/test/test_db.py b/auslib/test/test_db.py index 386f201c30..5936e3f86f 100644 --- a/auslib/test/test_db.py +++ b/auslib/test/test_db.py @@ -4398,13 +4398,16 @@ def testCannotRevokeRoleThatMakesRequiredSignoffImpossible(self): self.permissions.revokeRole, "bob", "dev", "bill", old_data_version=1) def testGetAllUsers(self): - self.assertEquals(set(self.permissions.getAllUsers()), set(["bill", - "bob", - "cathy", - "fred", - "george", - "janet", - "sean"])) + self.assertEquals(self.permissions.getAllUsers(), ({ + 'bill': {'roles': []}, + 'bob': {'roles': [ + {'data_version': 1, 'role': 'dev'}, + {'data_version': 1, 'role': 'releng'}]}, + 'cathy': {'roles': [{'data_version': 1, 'role': 'releng'}]}, + 'fred': {'roles': []}, + 'george': {'roles': []}, + 'janet': {'roles': [{'data_version': 1, 'role': 'releng'}]}, + 'sean': {'roles': []}})) def testCountAllUsers(self): self.assertEquals(self.permissions.countAllUsers(), 7) diff --git a/auslib/web/admin/swagger/api.yaml b/auslib/web/admin/swagger/api.yaml index 5892e12c15..c4f412f330 100644 --- a/auslib/web/admin/swagger/api.yaml +++ b/auslib/web/admin/swagger/api.yaml @@ -251,6 +251,46 @@ parameters: # No need to validate the duplicate values again. additionalProperties: true + changedByParam: + name: changed_by + in: query + description: The username who made the requested changes. + type: string + x-nullable: true + required: false + + changeIdParam: + name: change_id + in: query + description: The change_id value to filter hsitory by. + type: integer + x-nullable: true + required: false + + historyDataVersionParam: + name: data_version + in: query + description: data_version field value to filter history by. + type: integer + x-nullable: true + required: false + + timestampFromParam: + name: timestamp_from + in: query + description: timestamp from/above which to filter history. + type: integer + x-nullable: true + required: false + + timestampToParam: + name: timestamp_to + in: query + description: timestamp to/below which history is filtered. + type: integer + x-nullable: true + required: false + responses: updateExistingObject: description: "successfully updated the existing object and incremented its data_version" @@ -350,6 +390,253 @@ paths: examples: text/html: "csrf_token: 1491342563##c4e6fef0b978e6c89af9ff1015e67b9ca7c45d14" + /rules/history: + get: + summary: Returns a list of all histories. + description: > + Returns a list of rules history. + tags: + - "History Histories Rules" + operationId: auslib.web.common.history_all.get_rules_history + consumes: [] + produces: [application/json] + externalDocs: + url: "http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#put" + description: "Returns a list of rules history" + parameters: + - $ref: '#/parameters/productParam' + - $ref: '#/parameters/limitParam' + - $ref: '#/parameters/pageParam' + - $ref: '#/parameters/changedByParam' + - $ref: '#/parameters/changeIdParam' + - $ref: '#/parameters/historyDataVersionParam' + - $ref: '#/parameters/timestampFromParam' + - $ref: '#/parameters/timestampToParam' + - name: channel + in: query + description: channel stirng for filtering history. + type: string + x-nullable: true + required: false + responses: + '200': + description: Successfully fetched rules history + '400': + $ref: '#/responses/invalidFormData' + + /releases/history: + get: + summary: Returns a list of releases history. + description: > + Returns a list of releases history. + tags: + - "History Histories Releases" + operationId: auslib.web.common.history_all.get_releases_history + consumes: [] + produces: [application/json] + externalDocs: + url: "http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#put" + description: "Returns a list of releases history" + parameters: + - $ref: '#/parameters/productParam' + - $ref: '#/parameters/limitParam' + - $ref: '#/parameters/pageParam' + - $ref: '#/parameters/changedByParam' + - $ref: '#/parameters/changeIdParam' + - $ref: '#/parameters/historyDataVersionParam' + - $ref: '#/parameters/timestampFromParam' + - $ref: '#/parameters/timestampToParam' + responses: + '200': + description: Successfully fetched histories + '400': + $ref: '#/responses/invalidFormData' + + /permissions/history: + get: + summary: Returns a list of permissions history. + description: > + Returns a list of permissions history. + tags: + - "History Histories Permissions" + operationId: auslib.web.common.history_all.get_permissions_history + consumes: [] + produces: [application/json] + externalDocs: + url: "http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#put" + description: "Returns a list of rules, releases and permissions histories" + parameters: + - $ref: '#/parameters/limitParam' + - $ref: '#/parameters/pageParam' + - $ref: '#/parameters/changedByParam' + - $ref: '#/parameters/changeIdParam' + - $ref: '#/parameters/historyDataVersionParam' + - $ref: '#/parameters/timestampFromParam' + - $ref: '#/parameters/timestampToParam' + responses: + '200': + description: Successfully fetched permissions history + '400': + $ref: '#/responses/invalidFormData' + + /rules_scheduled_change/history: + get: + summary: Returns a list of rules scheduled change history. + description: > + Returns rules scheduled change history. + tags: + - "History Rules Scheduled Change" + operationId: auslib.web.common.history_all.get_rules_scheduled_change_history + consumes: [] + produces: [application/json] + externalDocs: + url: "http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#put" + description: "Returns a list of rules scheduled change history" + parameters: + - $ref: '#/parameters/productParam' + - $ref: '#/parameters/limitParam' + - $ref: '#/parameters/pageParam' + - $ref: '#/parameters/changedByParam' + - $ref: '#/parameters/changeIdParam' + - $ref: '#/parameters/historyDataVersionParam' + - $ref: '#/parameters/timestampFromParam' + - $ref: '#/parameters/timestampToParam' + responses: + '200': + description: Successfully fetched rules scheduled change history + '400': + $ref: '#/responses/invalidFormData' + + /releases_scheduled_change/history: + get: + summary: Returns a list of releases scheduled change history. + description: > + Returns scheduled change histories. + tags: + - "History Releases Scheduled Change" + operationId: auslib.web.common.history_all.get_releases_scheduled_change_history + consumes: [] + produces: [application/json] + externalDocs: + url: "http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#put" + description: "Returns a list of releases scheduled change history" + parameters: + - $ref: '#/parameters/productParam' + - $ref: '#/parameters/limitParam' + - $ref: '#/parameters/pageParam' + - $ref: '#/parameters/changedByParam' + - $ref: '#/parameters/changeIdParam' + - $ref: '#/parameters/historyDataVersionParam' + - $ref: '#/parameters/timestampFromParam' + - $ref: '#/parameters/timestampToParam' + responses: + '200': + description: Successfully fetched releases scheduled change history + '400': + $ref: '#/responses/invalidFormData' + + /permissions_scheduled_change/history: + get: + summary: Returns a list of permissions scheduled change history. + description: > + Returns permissions scheduled change history. + tags: + - "History SC" + operationId: auslib.web.common.history_all.get_permissions_scheduled_change_history + consumes: [] + produces: [application/json] + externalDocs: + url: "http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#put" + description: "Returns a list of permissions scheduled changes history" + parameters: + - $ref: '#/parameters/productParam' + - $ref: '#/parameters/limitParam' + - $ref: '#/parameters/pageParam' + - $ref: '#/parameters/changedByParam' + - $ref: '#/parameters/changeIdParam' + - $ref: '#/parameters/historyDataVersionParam' + - $ref: '#/parameters/timestampFromParam' + - $ref: '#/parameters/timestampToParam' + responses: + '200': + description: Successfully fetched permissions scheduled changes history + '400': + $ref: '#/responses/invalidFormData' + + /permission_required_signoffs_scheduled_change/history: + get: + summary: Returns a list of permissions required signoff scheduled change history. + description: > + Returns permissions required signoff scheduled change history. + tags: + - "History Permissions Scheduled Change Signoff" + operationId: auslib.web.common.history_all.get_permission_required_signoffs_scheduled_change_history + consumes: [] + produces: [application/json] + externalDocs: + url: "http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#put" + description: "Returns a list of permissions required signoff scheduled change history" + parameters: + - $ref: '#/parameters/productParam' + - $ref: '#/parameters/limitParam' + - $ref: '#/parameters/pageParam' + - $ref: '#/parameters/changedByParam' + - $ref: '#/parameters/changeIdParam' + - $ref: '#/parameters/historyDataVersionParam' + - $ref: '#/parameters/timestampFromParam' + - $ref: '#/parameters/timestampToParam' + - name: role + in: query + description: The role(s) required for permissions scheduled change + type: string + x-nullable: true + required: false + responses: + '200': + description: Successfully fetched permissions required signoff scheduled change history + '400': + $ref: '#/responses/invalidFormData' + + /product_required_signoffs_scheduled_change/history: + get: + summary: Returns a list of product required signoff scheduled change history. + description: > + Returns product required signoff scheduled change history. + tags: + - "History Product Signoff Scheduled Change" + operationId: auslib.web.common.history_all.get_product_required_signoffs_scheduled_change_history + consumes: [] + produces: [application/json] + externalDocs: + url: "http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#put" + description: "Returns a list of product required signoff scheduled change history" + parameters: + - $ref: '#/parameters/productParam' + - $ref: '#/parameters/limitParam' + - $ref: '#/parameters/pageParam' + - $ref: '#/parameters/changedByParam' + - $ref: '#/parameters/changeIdParam' + - $ref: '#/parameters/historyDataVersionParam' + - $ref: '#/parameters/timestampFromParam' + - $ref: '#/parameters/timestampToParam' + - name: channel + in: query + description: channel stirng for filtering history. + type: string + x-nullable: true + required: false + - name: role + in: query + description: The role(s) required product scheduled change + type: string + x-nullable: true + required: false + responses: + '200': + description: Successfully fetched product required signoff scheduled change history + '400': + $ref: '#/responses/invalidFormData' + /history/diff/release/{change_id}/{field}: get: summary: Returns a diff of the value of the named field @@ -1025,6 +1312,40 @@ paths: '404': $ref: '#/responses/resourceNotFound' + /required_signoffs/product/history: + get: + summary: Returns a list of product required signoff history. + description: > + Returns product required signoff history. + tags: + - "History Signoff Product" + operationId: auslib.web.common.history_all.get_product_required_signoffs_history + consumes: [] + produces: [application/json] + externalDocs: + url: "http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#put" + description: "Returns a list of product required signoff history" + parameters: + - $ref: '#/parameters/productParam' + - $ref: '#/parameters/limitParam' + - $ref: '#/parameters/pageParam' + - $ref: '#/parameters/changedByParam' + - $ref: '#/parameters/changeIdParam' + - $ref: '#/parameters/historyDataVersionParam' + - $ref: '#/parameters/timestampFromParam' + - $ref: '#/parameters/timestampToParam' + - name: role + in: query + description: The role(s) required for product signoff + type: string + x-nullable: true + required: false + responses: + '200': + description: Successfully fetched product required signoff history + '400': + $ref: '#/responses/invalidFormData' + /required_signoffs/permissions: get: summary: Returns all the permissions required signoffs @@ -1173,6 +1494,40 @@ paths: '404': $ref: '#/responses/resourceNotFound' + /required_signoffs/permissions/history: + get: + summary: Returns a list of permissions required signoff history. + description: > + Returns permissions required signoff history. + tags: + - "History Permissions Signoff" + operationId: auslib.web.common.history_all.get_permission_required_signoffs_history + consumes: [] + produces: [application/json] + externalDocs: + url: "http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#put" + description: "Returns a list of permissions required signoff history" + parameters: + - $ref: '#/parameters/productParam' + - $ref: '#/parameters/limitParam' + - $ref: '#/parameters/pageParam' + - $ref: '#/parameters/changedByParam' + - $ref: '#/parameters/changeIdParam' + - $ref: '#/parameters/historyDataVersionParam' + - $ref: '#/parameters/timestampFromParam' + - $ref: '#/parameters/timestampToParam' + - name: role + in: query + description: The role(s) required for permission signoff + type: string + x-nullable: true + required: false + responses: + '200': + description: Successfully fetched permissions required signoff history + '400': + $ref: '#/responses/invalidFormData' + /users: get: summary: Returns all of Users in Balrog's DB @@ -1189,7 +1544,7 @@ paths: description: "Returns all of Users in Balrog's DB." responses: '200': - description: Get all Users + description: Get all Users and their Roles schema: type: object properties: @@ -1199,37 +1554,21 @@ paths: uniqueItems: true minItems: 0 items: - type: string - example: ["balrogadmin"] + type: object + example: { + "balrogadmin": { + "roles":[ + { + "data_version": 1, + "role": "releng" + }, + { + "data_version": 1, + "role": "relman" + }] + } + } - /users/roles: - get: - summary: Returns all distinct user roles - description: > - Returns all unique roles defined for all users. [Docs](http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#users-username-roles). - tags: - - Users - operationId: auslib.web.admin.views.mapper.all_users_roles_get - consumes: [] - produces: - - application/json - externalDocs: - url: "http://mozilla-balrog.readthedocs.io/en/latest/database.html#user-roles" - description: Returns all distinct user roles - responses: - '200': - description: Successfully fetched all user roles. - schema: - type: object - properties: - roles: - description: list of roles - type: array - uniqueItems: true - minItems: 0 - items: - type: string - example: ["releng", "qa"] /users/{username}: get: @@ -1291,6 +1630,7 @@ paths: '403': $ref: '#/responses/unauthorizedUser' + /users/{username}/permissions: get: summary: Returns all of the details about the user @@ -1319,40 +1659,6 @@ paths: options: actions: ['create', 'modify'] - /users/{username}/roles: - get: - summary: Returns list of user roles - description: > - Returns list of user roles as a json object. [Docs](http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#users-username-roles). - tags: - - Users - operationId: auslib.web.admin.views.mapper.user_get_roles - consumes: [] - produces: - - application/json - externalDocs: - url: "http://mozilla-balrog.readthedocs.io/en/latest/admin_api.html#id26" - description: Returns list of user roles - parameters: - - $ref: '#/parameters/usernameParam' - responses: - '200': - description: Successfully fetched the list of user roles. - schema: - type: object - properties: - roles: - description: list of roles - type: array - uniqueItems: true - minItems: 0 - items: - allOf: - - $ref: '#/definitions/DataVersionModel' - - $ref: '#/definitions/RoleModel' - required: - - data_version - - role /users/{username}/roles/{role}: put: diff --git a/auslib/web/admin/views/mapper.py b/auslib/web/admin/views/mapper.py index a5100702cb..cee00a12d8 100644 --- a/auslib/web/admin/views/mapper.py +++ b/auslib/web/admin/views/mapper.py @@ -4,8 +4,8 @@ RuleHistoryAPIView, RuleScheduledChangesView, EnactRuleScheduledChangeView, RuleScheduledChangeSignoffsView, \ RuleScheduledChangeView, RuleScheduledChangeHistoryView -from auslib.web.admin.views.permissions import UsersView, AllRolesView, SpecificUserView,\ - PermissionsView, UserRolesView, UserRoleView, SpecificPermissionView, PermissionScheduledChangesView, \ +from auslib.web.admin.views.permissions import UsersView, SpecificUserView,\ + PermissionsView, UserRoleView, SpecificPermissionView, PermissionScheduledChangesView, \ EnactPermissionScheduledChangeView, PermissionScheduledChangeSignoffsView, PermissionScheduledChangeView, \ PermissionScheduledChangeHistoryView @@ -63,11 +63,6 @@ def users_get(): return UsersView().get() -def all_users_roles_get(): - """GET /users/roles""" - return AllRolesView().get() - - def specific_user_get(username): """GET /users/:username""" return SpecificUserView().get(username) @@ -78,11 +73,6 @@ def user_permissions_get(username): return PermissionsView().get(username) -def user_get_roles(username): - """GET /users/:username/roles""" - return UserRolesView().get(username) - - def user_role_put(username, role): """PUT /users/:username/roles/:role""" return UserRoleView().put(username, role) diff --git a/auslib/web/admin/views/permissions.py b/auslib/web/admin/views/permissions.py index 0f719a1a68..9020daa6a3 100644 --- a/auslib/web/admin/views/permissions.py +++ b/auslib/web/admin/views/permissions.py @@ -19,7 +19,8 @@ def get(self): self.log.debug("Found users: %s", users) # We don't return a plain jsonify'ed list here because of: # http://flask.pocoo.org/docs/security/#json-security - return jsonify(dict(users=users)) + # return jsonify(dict(users=users)) + return jsonify(users) class SpecificUserView(AdminView): @@ -261,22 +262,6 @@ def _post(self, sc_id, transaction, changed_by): return super(PermissionScheduledChangeHistoryView, self)._post(sc_id, transaction, changed_by) -class UserRolesView(AdminView): - """/users/:username/roles""" - - def get(self, username): - roles = dbo.permissions.getUserRoles(username) - return jsonify({"roles": roles}) - - -class AllRolesView(AdminView): - """/users/roles""" - - def get(self): - roles = dbo.permissions.getAllRoles() - return jsonify({"roles": roles}) - - class UserRoleView(AdminView): """/users/:username/roles/:role""" diff --git a/auslib/web/admin/views/required_signoffs.py b/auslib/web/admin/views/required_signoffs.py index 472b0874bc..3e58968c71 100644 --- a/auslib/web/admin/views/required_signoffs.py +++ b/auslib/web/admin/views/required_signoffs.py @@ -13,6 +13,8 @@ ScheduledChangeHistoryView from auslib.db import SignoffRequiredError from auslib.global_state import dbo +from auslib.web.common.history import get_input_dict +from sqlalchemy import and_ class RequiredSignoffsView(AdminView): @@ -48,6 +50,19 @@ def __init__(self, table, decisionFields): self.decisionFields = decisionFields super(RequiredSignoffsHistoryAPIView, self).__init__(table=table) + def _get_filters(self): + query = get_input_dict() + where = [getattr(self.table.history, f) == query.get(f) for f in query] + where.append(self.table.history.data_version != null()) + if hasattr(self.history_table, 'product'): + where.append(self.history_table.product != null()) + request = connexion.request + if request.args.get('timestamp_from'): + where.append(self.history_table.timestamp >= int(request.args.get('timestamp_from'))) + if request.args.get('timestamp_to'): + where.append(self.history_table.timestamp <= int(request.args.get('timestamp_to'))) + return where + def get(self, input_dict): if not self.table.select({f: input_dict.get(f) for f in self.decisionFields}): return problem(404, "Not Found", "Requested Required Signoff does not exist") @@ -74,6 +89,27 @@ def get(self, input_dict): return jsonify(count=total_count, required_signoffs=revisions) + def get_all(self): + try: + page = int(connexion.request.args.get('page', 1)) + limit = int(connexion.request.args.get('limit', 100)) + except ValueError as msg: + self.log.warning("Bad input: %s", msg) + return problem(400, "Bad Request", str(msg)) + offset = limit * (page - 1) + + where = self._get_filters() + total_count = self.table.history.t.count()\ + .where(and_(*where))\ + .execute().fetchone()[0] + + revisions = self.table.history.select( + where=where, limit=limit, offset=offset, + order_by=[self.table.history.timestamp.desc()] + ) + + return jsonify(count=total_count, required_signoffs=revisions) + class ProductRequiredSignoffsView(RequiredSignoffsView): """/required_signoffs/product""" diff --git a/auslib/web/admin/views/scheduled_changes.py b/auslib/web/admin/views/scheduled_changes.py index 1a010fe6c4..59600fa4ef 100644 --- a/auslib/web/admin/views/scheduled_changes.py +++ b/auslib/web/admin/views/scheduled_changes.py @@ -7,6 +7,7 @@ from auslib.web.admin.views.history import HistoryView from auslib.web.admin.views.problem import problem from auslib.web.admin.views.validators import is_when_present_and_in_past_validator +from auslib.web.common.history import get_input_dict class ScheduledChangesView(AdminView): @@ -200,6 +201,20 @@ def _get_filters(self, sc): return [self.history_table.sc_id == sc['sc_id'], self.history_table.data_version != null()] + def _get_filters_all(self, obj): + query = get_input_dict() + where = [False, False] + where = [getattr(self.history_table, f) == query.get(f) for f in query] + where.append(self.history_table.data_version != null()) + request = connexion.request + if hasattr(self.history_table, 'product'): + where.append(self.history_table.product != null()) + if request.args.get('timestamp_from'): + where.append(self.history_table.timestamp >= int(request.args.get('timestamp_from'))) + if request.args.get('timestamp_to'): + where.append(self.history_table.timestamp <= int(request.args.get('timestamp_to'))) + return where + def _get_what(self, change, changed_by, transaction): # There's a big 'ol assumption here that the primary Scheduled Changes # table and the conditions table always keep their data version in sync. @@ -241,6 +256,18 @@ def get(self, sc_id): self.log.warning("Bad input: %s", msg) return problem(400, "Bad Request", "Error in fetching revisions", ext={"exception": msg}) + def get_all(self): + try: + return self.get_revisions( + get_object_callback=lambda: ScheduledChangesView.get, + history_filters_callback=self._get_filters_all, + process_revisions_callback=self._process_revisions, + revisions_order_by=[self.history_table.timestamp.desc()], + obj_not_found_msg='Scheduled change does not exist') + except (ValueError, AssertionError) as msg: + self.log.warning("Bad input: %s", msg) + return problem(400, "Bad Request", "Error in fetching revisions", ext={"exception": msg}) + def _post(self, sc_id, transaction, changed_by): return self.revert_to_revision( get_object_callback=lambda: self._get_sc(sc_id), diff --git a/auslib/web/common/history.py b/auslib/web/common/history.py index d11cd415e5..6288acf0e8 100644 --- a/auslib/web/common/history.py +++ b/auslib/web/common/history.py @@ -53,6 +53,20 @@ def get_history(self, response_key='revisions'): return jsonify(ret) +def get_input_dict(): + reserved_filter_params = ['limit', 'page', 'timestamp_from', 'timestamp_to'] + args = request.args + query_keys = [] + query = {} + for key in args: + if key not in reserved_filter_params: + query_keys.append(key) + + for key in query_keys: + query[key] = request.args.get(key) + return query + + history_keys = ('timestamp', 'change_id', 'data_version', 'changed_by') diff --git a/auslib/web/common/history_all.py b/auslib/web/common/history_all.py new file mode 100644 index 0000000000..be9ae28a83 --- /dev/null +++ b/auslib/web/common/history_all.py @@ -0,0 +1,101 @@ +import logging +from auslib.global_state import dbo +from connexion import problem, request +from sqlalchemy.sql.expression import null +from auslib.web.common.history import HistoryHelper, get_input_dict +from auslib.web.common.rules import get_rules +from auslib.web.common.releases import get_releases, process_release_revisions +from auslib.web.admin.views.permissions import UsersView, PermissionScheduledChangeHistoryView +from auslib.web.admin.views.rules import RuleScheduledChangeHistoryView +from auslib.web.admin.views.releases import ReleaseScheduledChangeHistoryView +from auslib.web.admin.views.required_signoffs import ProductRequiredSignoffsHistoryAPIView, \ + PermissionsRequiredSignoffsHistoryAPIView, ProductRequiredSignoffScheduledChangeHistoryView, \ + PermissionsRequiredSignoffScheduledChangeHistoryView + + +log = logging.getLogger(__name__) + + +def _get_filters(obj, history_table): + query = get_input_dict() + where = [False, False] + where = [getattr(history_table, f) == query.get(f) for f in query] + where.append(history_table.data_version != null()) + if hasattr(history_table, 'product'): + where.append(history_table.product != null()) + if request.args.get('timestamp_from'): + where.append(history_table.timestamp >= int(request.args.get('timestamp_from'))) + if request.args.get('timestamp_to'): + where.append(history_table.timestamp <= int(request.args.get('timestamp_to'))) + return where + + +def _get_histories(table, obj, process_revisions_callback=None): + history_table = table + order_by = [history_table.timestamp.desc()] + history_helper = HistoryHelper(hist_table=history_table, + order_by=order_by, + get_object_callback=lambda: obj, + history_filters_callback=_get_filters, + obj_not_found_msg='No history found', + process_revisions_callback=process_revisions_callback) + try: + return history_helper.get_history() + except (ValueError, AssertionError) as msg: + log.warning("Bad input: %s", msg) + return problem(400, "Bad Request", "Error occurred when trying to fetch histories", + ext={"exception": str(msg)}) + + +def get_rules_history(): + """GET /rules/history""" + history_table = dbo.rules.history + return _get_histories(history_table, get_rules) + + +def get_releases_history(): + """GET /releases/history""" + history_table = dbo.releases.history + return _get_histories(history_table, get_releases, process_release_revisions) + + +def get_permissions_history(): + """GET /permissions/history""" + history_table = dbo.permissions.history + get_permissions = UsersView().get() + return _get_histories(history_table, get_permissions) + + +def get_permissions_scheduled_change_history(): + """GET /permissions_scheduled_change/history""" + return PermissionScheduledChangeHistoryView().get_all() + + +def get_rules_scheduled_change_history(): + """GET /rules_scheduled_change/history""" + return RuleScheduledChangeHistoryView().get_all() + + +def get_releases_scheduled_change_history(): + """GET /releases_scheduled_change/history""" + return ReleaseScheduledChangeHistoryView().get_all() + + +def get_product_required_signoffs_scheduled_change_history(): + """GET /product_required_signoffs_scheduled_change/history""" + return ProductRequiredSignoffScheduledChangeHistoryView().get_all() + + +def get_permission_required_signoffs_scheduled_change_history(): + """GET /permissions_required_signoff_scheduled_change/history""" + return PermissionsRequiredSignoffScheduledChangeHistoryView().get_all() + + +def get_product_required_signoffs_history(): + """GET /required_signoffs/product/history""" + return ProductRequiredSignoffsHistoryAPIView().get_all() + + +def get_permission_required_signoffs_history(): + """GET /required_signoffs/permissions/history""" + return PermissionsRequiredSignoffsHistoryAPIView().get_all() diff --git a/requirements.txt b/requirements.txt index 06a4278bb0..3fdd4ac7c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -123,7 +123,8 @@ clickclick==1.2.1 \ strict-rfc3339==0.7 \ --hash=sha256:5cad17bedfc3af57b399db0fed32771f18fc54bbd917e85546088607ac5e1277 swagger-spec-validator==2.1.0 \ - --hash=sha256:dc9219c6572ce0def6e1c160ca253c0e7fcde75812628f0c0199334f85bd138e + --hash=sha256:dc9219c6572ce0def6e1c160ca253c0e7fcde75812628f0c0199334f85bd138e \ + --hash=sha256:aedacb6c6b475026a1b5ac218fb590382d08064e227da254eb961d17cfd2b7c1 pathlib==1.0.1 \ --hash=sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f inflection==0.3.1 \ diff --git a/ui/app/css/style.less b/ui/app/css/style.less index df12741f3f..80d5225e10 100644 --- a/ui/app/css/style.less +++ b/ui/app/css/style.less @@ -26,7 +26,14 @@ button { background-color: #fff !important; } -.btn-asap{ +.btn-asap { background-image: linear-gradient(-180deg, #fafbfc 0%, #eff3f6 90%); z-index: 1 !important; } +.role-dropdown { + margin-top: 30px; + margin-left: 80px; +} +#role-select-box{ + width: 30%; +} diff --git a/ui/app/js/controllers/permissions_controller.js b/ui/app/js/controllers/permissions_controller.js index df4885a1cc..d28c596625 100644 --- a/ui/app/js/controllers/permissions_controller.js +++ b/ui/app/js/controllers/permissions_controller.js @@ -6,35 +6,70 @@ function($scope, $routeParams, $location, $timeout, Permissions, Search, $modal, $scope.loading = true; $scope.failed = false; $scope.username = $routeParams.username; + $scope.users = []; + $scope.tab = 1; + + if ($scope.username) { // history of a specific rule Permissions.getUserPermissions($scope.username) - .then(function(response) { - $scope.permissions = response; - }); + .then(function (response) { + $scope.permissions = response; + }); } else { Permissions.getUsers() - .success(function(response) { - $scope.users = _.map(response.users, function (each) { - return {username: each}; + .success(function (response) { + var users = []; + var user = Object.keys(response); + user.forEach(function(eachUser){ + var eachElement = {}; + eachElement['roles'] = response[eachUser].roles; + eachElement['username'] = eachUser; + users.push(eachElement); + }); + $scope.users = users; + }) + .error(function () { + console.error(arguments); + $scope.failed = true; + }) + .finally(function () { + $scope.loading = false; + }); - $scope.permissions_count = $scope.users.length; - $scope.page_size_pair = [{id: 20, name: '20'}, - {id: 50, name: '50'}, - {id: $scope.permissions_count, name: 'All'}]; - }) - .error(function() { - console.error(arguments); - $scope.failed = true; - }) - .finally(function() { - $scope.loading = false; - }); + + $scope.permissions_count = $scope.users.length; + $scope.page_size_pair = [{ id: 20, name: '20' }, + { id: 50, name: '50' }, + { id: $scope.permissions_count, name: 'All' }]; + } + $scope.roles = function(){ + var roles = []; + $scope.users.forEach(function (eachUser){ + eachUser.roles.forEach(function(role){ + if(roles.indexOf(role.role)=== -1){ + roles.push(role.role); + } + }); + }); + return roles; + }; + + + $scope.filterUserByRole = function (role) { + return function (user) { + return user.roles.some(function(each){ + return each.role === role; + }); + }; + }; + + $scope.signoffRequirements = []; PermissionsRequiredSignoffs.getRequiredSignoffs() - .then(function(payload) { + .then(function (payload) { $scope.signoffRequirements = payload.data.required_signoffs; }); @@ -49,11 +84,11 @@ function($scope, $routeParams, $location, $timeout, Permissions, Search, $modal, search: $location.hash(), }; - $scope.hasFilter = function() { + $scope.hasFilter = function () { return !!(false || $scope.filters.search.length); }; - $scope.$watchCollection('filters.search', function(value) { + $scope.$watchCollection('filters.search', function (value) { $location.hash(value); Search.noticeSearchChange( value, @@ -66,18 +101,20 @@ function($scope, $routeParams, $location, $timeout, Permissions, Search, $modal, $scope.highlightSearch = Search.highlightSearch; // $scope.removeFilterSearchWord = Search.removeFilterSearchWord; - $scope.filterBySearch = function(item) { + $scope.filterBySearch = function (item) { // basically, look for a reason to NOT include this if (Search.word_regexes.length) { // every word in the word_regexes array needs to have some match var matches = 0; - _.each(Search.word_regexes, function(each) { + _.each(Search.word_regexes, function (each) { var regex = each[0]; var on = each[1]; // console.log(regex, on); - if ((on === '*' || on === 'username') && item.username && item.username.match(regex)) { - matches++; - return; + if ('username' in item) { + if ((on === '*' || on === 'username') && item.username && item.username.match(regex)) { + matches++; + return; + } } }); return matches === Search.word_regexes.length; @@ -87,7 +124,7 @@ function($scope, $routeParams, $location, $timeout, Permissions, Search, $modal, }; /* End filtering */ - $scope.openUpdateModal = function(user) { + $scope.openUpdateModal = function (user) { $scope.is_edit = true; var modalInstance = $modal.open({ templateUrl: 'permissions_modal.html', @@ -104,15 +141,18 @@ function($scope, $routeParams, $location, $timeout, Permissions, Search, $modal, user: function () { return user; }, - permissionSignoffRequirements: function() { + permissionSignoffRequirements: function () { return $scope.signoffRequirements; }, + roles: function() { + return $scope.roles(); + } } }); }; /* End openUpdateModal */ - $scope.openNewModal = function() { + $scope.openNewModal = function () { $scope.is_edit = false; var modalInstance = $modal.open({ templateUrl: 'permissions_modal.html', @@ -129,17 +169,20 @@ function($scope, $routeParams, $location, $timeout, Permissions, Search, $modal, user: function () { return $scope.user; }, - permissionSignoffRequirements: function() { + permissionSignoffRequirements: function () { return $scope.signoffRequirements; }, + roles: function() { + return $scope.roles(); + } } }); }; - /* End openNewModal */ + /* End openNewModal */ - $scope.openNewScheduledPermissionChangeModal = function(user) { + $scope.openNewScheduledPermissionChangeModal = function (user) { var modalInstance = $modal.open({ templateUrl: 'permissions_scheduled_change_modal.html', @@ -147,15 +190,15 @@ function($scope, $routeParams, $location, $timeout, Permissions, Search, $modal, size: 'lg', backdrop: 'static', resolve: { - scheduled_changes: function() { + scheduled_changes: function () { return []; }, - sc: function() { + sc: function () { sc = angular.copy(user); sc["change_type"] = "insert"; return sc; }, - permissionSignoffRequirements: function() { + permissionSignoffRequirements: function () { return $scope.signoffRequirements; }, } @@ -166,6 +209,4 @@ function($scope, $routeParams, $location, $timeout, Permissions, Search, $modal, Helpers.selectPageSize($scope, 'permissions_page_size'); }; - - }); diff --git a/ui/app/js/controllers/user_edit_controller.js b/ui/app/js/controllers/user_edit_controller.js index 83c2a0dfa5..249716c193 100644 --- a/ui/app/js/controllers/user_edit_controller.js +++ b/ui/app/js/controllers/user_edit_controller.js @@ -1,9 +1,12 @@ /*global sweetAlert swal */ angular.module('app').controller('UserPermissionsCtrl', -function ($scope, $modalInstance, CSRF, Permissions, users, is_edit, user, permissionSignoffRequirements) { +function ($scope, $modalInstance, CSRF, Permissions, users, roles, is_edit, user, permissionSignoffRequirements) { $scope.loading = true; $scope.users = users; + $scope.roles_list = roles; + $scope.originalPermissions = []; + $scope.currentItemTab = 1; $scope.is_edit = is_edit; @@ -14,44 +17,30 @@ function ($scope, $modalInstance, CSRF, Permissions, users, is_edit, user, permi $scope.errors = { permissions: {} }; - if($scope.is_edit){ + + if ($scope.is_edit) { $scope.original_user = user; $scope.user = angular.copy(user); + $scope.user.permissions = []; Permissions.getUserPermissions($scope.user.username) - .then(function(permissions) { - _.forEach(permissions, function(p) { - if (p.options) { - p.options_as_json = JSON.stringify(p['options']); - } + .then(function (permissions) { + _.forEach(permissions, function (p) { + if (p.options) { + p.options_as_json = JSON.stringify(p['options']); + } + }); + $scope.originalPermissions = angular.copy(permissions); + $scope.user.permissions = permissions; }); - $scope.originalPermissions = angular.copy(permissions); - $scope.user.permissions = permissions; - }); - $scope.user.roles = []; - Permissions.getUserRoles($scope.user.username) - .success(function(response) { - $scope.user.roles = response.roles; - }) - .error(function(response) { - if (typeof response === 'object') { - $scope.errors = response; - sweetAlert( - "Failed to load User Roles", - "error" - ); - } else if (typeof response === 'string') { - sweetAlert( - "Failed to load User Roles" + - "(" + response+ ")", - "error" - ); + $scope.users.forEach(function (eachUser) { + if (eachUser.username === $scope.user.username) { + $scope.user.roles = eachUser.roles; } - }) - .finally(function() { - $scope.loading = false; }); + $scope.loading = false; + } else { $scope.user = { @@ -63,7 +52,6 @@ function ($scope, $modalInstance, CSRF, Permissions, users, is_edit, user, permi } - $scope.roles_list = []; function fromFormData(permission) { permission = angular.copy(permission); try { @@ -96,27 +84,6 @@ function ($scope, $modalInstance, CSRF, Permissions, users, is_edit, user, permi }); }, true); - Permissions.getAllRoles() - .success(function(response) { - $scope.roles_list = response.roles; - }) - .error(function(response) { - if (typeof response === 'object') { - $scope.errors = response; - sweetAlert( - "Form submission error", - "See fields highlighted in red.", - "error" - ); - } else if (typeof response === 'string') { - sweetAlert( - "Form submission error", - "Unable to submit successfully.\n" + - "(" + response+ ")", - "error" - ); - } - }); $scope.saving = false; $scope.usersaved = false; @@ -128,7 +95,6 @@ function ($scope, $modalInstance, CSRF, Permissions, users, is_edit, user, permi $scope.$watchCollection('permission', function(value) { value.options = value.options_as_json; }); - $scope.grantRole = function() { $scope.saving = true; CSRF.getToken() @@ -137,7 +103,6 @@ function ($scope, $modalInstance, CSRF, Permissions, users, is_edit, user, permi .success(function(response) { $scope.role.data_version = response.new_data_version; $scope.user.roles.push($scope.role); - if (!($scope.role.role in $scope.roles_list)) { $scope.roles_list.push($scope.role.role); } diff --git a/ui/app/js/services/permissions_service.js b/ui/app/js/services/permissions_service.js index f9390d226c..22f1803ee6 100644 --- a/ui/app/js/services/permissions_service.js +++ b/ui/app/js/services/permissions_service.js @@ -57,18 +57,6 @@ angular.module("app").factory('Permissions', function($http, $q, ScheduledChange return $http.post("/api/scheduled_changes/permissions", data); }, - getUserRoles: function(username) { - // What comes back from the server is a dict like this: - // {'roles': ['role1', 'role2'...]} if the user has roles - // otherwise the value of the dict will be an empty array: - // {'roles': []} - // when the user has no roles - var url = '/api/users/' + encodeURIComponent(username) + '/roles'; - return $http.get(url); - }, - getAllRoles: function() { - return $http.get('/api/users/roles'); - }, grantRole: function(username, role, data_version, csrf_token) { var url = '/api/users/' + encodeURIComponent(username) + '/roles/'; url += encodeURIComponent(role); diff --git a/ui/app/templates/permissions.html b/ui/app/templates/permissions.html index a3829ffaa6..239665a540 100644 --- a/ui/app/templates/permissions.html +++ b/ui/app/templates/permissions.html @@ -1,41 +1,50 @@ -
+