Skip to content

Commit 09a288d

Browse files
authored
add job user for an org (#189)
* add functions to create new user in auth0 and exchange tokens * use FLaskGroup so another app object isn't created * add procedures to store token and create job user * add admin cli command to create new job user for org * test admincli * add new auth0_info tests * add procedures to create job roles * add procedure to grant a job role to job user * add cli command to add job role to user
1 parent e347419 commit 09a288d

File tree

8 files changed

+682
-9
lines changed

8 files changed

+682
-9
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
DROP PROCEDURE grant_job_role;
2+
DROP PROCEDURE create_forecast_generation_role;
3+
DROP PROCEDURE create_data_validation_role;
4+
DROP PROCEDURE create_report_creation_role;
5+
DROP PROCEDURE create_job_user;
6+
DROP PROCEDURE store_token;
7+
DROP USER 'token_user'@'localhost';
8+
DROP TABLE arbiter_data.job_tokens;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
CREATE TABLE arbiter_data.job_tokens(
2+
id BINARY(16) NOT NULL,
3+
token VARCHAR(256) NOT NULL,
4+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
5+
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
6+
7+
PRIMARY KEY (id),
8+
FOREIGN KEY (id)
9+
REFERENCES users(id)
10+
ON DELETE CASCADE ON UPDATE RESTRICT
11+
) ENGINE=INNODB ENCRYPTION='Y' ROW_FORMAT=COMPRESSED;
12+
13+
14+
CREATE USER 'token_user'@'localhost' IDENTIFIED WITH caching_sha2_password as '$A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED' ACCOUNT LOCK;
15+
16+
17+
CREATE DEFINER = 'token_user'@'localhost' PROCEDURE store_token (IN auth0id VARCHAR(32), IN token VARCHAR(256))
18+
COMMENT 'Store the encrypted token in the table'
19+
MODIFIES SQL DATA SQL SECURITY DEFINER
20+
BEGIN
21+
DECLARE user_id BINARY(16);
22+
SET user_id = (SELECT id FROM arbiter_data.users WHERE auth0_id = auth0id);
23+
INSERT INTO arbiter_data.job_tokens (id, token) VALUES (user_id, token)
24+
ON DUPLICATE KEY UPDATE token = VALUES(token);
25+
END;
26+
27+
GRANT SELECT(id, auth0_id) ON arbiter_data.users TO 'token_user'@'localhost';
28+
GRANT SELECT, INSERT, UPDATE(token) ON arbiter_data.job_tokens TO 'token_user'@'localhost';
29+
GRANT EXECUTE ON PROCEDURE arbiter_data.store_token TO 'token_user'@'localhost';
30+
GRANT EXECUTE ON PROCEDURE arbiter_data.store_token TO 'frameworkadmin'@'%';
31+
32+
33+
CREATE DEFINER = 'insert_rbac'@'localhost' PROCEDURE create_job_user(IN auth0id VARCHAR(32), IN orgid CHAR(36))
34+
COMMENT 'Inserts a new job user into the organization and add permission to read reference data only, returning new user id'
35+
MODIFIES SQL DATA SQL SECURITY DEFINER
36+
BEGIN
37+
DECLARE userid BINARY(16);
38+
DECLARE binorgid BINARY(16);
39+
SET userid = UUID_TO_BIN(UUID(), 1);
40+
SET binorgid = UUID_TO_BIN(orgid, 1);
41+
INSERT INTO arbiter_data.users (id, auth0_id, organization_id) VALUES (
42+
userid, auth0id, binorgid);
43+
CALL arbiter_data.add_reference_role_to_user(userid);
44+
CALL arbiter_data.create_default_user_role(userid, binorgid);
45+
SELECT BIN_TO_UUID(userid, 1);
46+
END;
47+
48+
GRANT EXECUTE ON PROCEDURE arbiter_data.create_job_user TO 'insert_rbac'@'localhost';
49+
GRANT EXECUTE ON PROCEDURE arbiter_data.create_job_user TO 'frameworkadmin'@'%';
50+
51+
52+
-- role for automatic report generation
53+
CREATE DEFINER = 'insert_rbac'@'localhost' PROCEDURE create_report_creation_role(IN orgid BINARY(16))
54+
COMMENT 'Create a role that provides the necessary access to create reports from any fx/obs'
55+
MODIFIES SQL DATA SQL SECURITY DEFINER
56+
BEGIN
57+
-- relying on default permissions already created on create organization
58+
DECLARE roleid BINARY(16);
59+
SET roleid = UUID_TO_BIN(UUID(), 1);
60+
INSERT INTO arbiter_data.roles (name, description, id, organization_id) VALUES (
61+
'Create reports', 'Create reports for any pairs of forecasts/prob. forecasts/observations/aggregates',
62+
roleid, orgid);
63+
INSERT INTO arbiter_data.role_permission_mapping (role_id, permission_id)
64+
SELECT roleid, id FROM arbiter_data.permissions WHERE applies_to_all AND organization_id = orgid
65+
AND description IN (
66+
'Read all sites', 'Read all observations', 'Read all observation values', 'Read all forecasts',
67+
'Read all forecast values', 'Read all probabilistic forecasts', 'Read all probabilistic forecast values',
68+
'Read all aggregates', 'Read all aggregate values', 'Create reports');
69+
SELECT BIN_TO_UUID(roleid, 1);
70+
END;
71+
72+
GRANT SELECT(id, applies_to_all, description, organization_id) ON arbiter_data.permissions TO 'insert_rbac'@'localhost';
73+
GRANT EXECUTE ON PROCEDURE arbiter_data.create_report_creation_role TO 'insert_rbac'@'localhost';
74+
GRANT EXECUTE ON PROCEDURE arbiter_data.create_report_creation_role TO 'frameworkadmin'@'%';
75+
76+
77+
-- role for data validation
78+
CREATE DEFINER = 'insert_rbac'@'localhost' PROCEDURE create_data_validation_role(IN orgid BINARY(16))
79+
COMMENT 'Create a role that provides the necessary access (read, read_values, write_values) to validate observations'
80+
MODIFIES SQL DATA SQL SECURITY DEFINER
81+
BEGIN
82+
-- relying on default permissions already created on create organization
83+
DECLARE roleid BINARY(16);
84+
SET roleid = UUID_TO_BIN(UUID(), 1);
85+
INSERT INTO arbiter_data.roles (name, description, id, organization_id) VALUES (
86+
'Validate observations', 'Enable observation data validation for all observations',
87+
roleid, orgid);
88+
INSERT INTO arbiter_data.role_permission_mapping (role_id, permission_id)
89+
SELECT roleid, id FROM arbiter_data.permissions WHERE applies_to_all AND organization_id = orgid
90+
AND description IN (
91+
'Read all sites', 'Read all observations', 'Read all observation values',
92+
'Submit values to all observations');
93+
SELECT BIN_TO_UUID(roleid, 1);
94+
END;
95+
96+
GRANT EXECUTE ON PROCEDURE arbiter_data.create_data_validation_role TO 'insert_rbac'@'localhost';
97+
GRANT EXECUTE ON PROCEDURE arbiter_data.create_data_validation_role TO 'frameworkadmin'@'%';
98+
99+
100+
-- role for automatic forecast generation
101+
CREATE DEFINER = 'insert_rbac'@'localhost' PROCEDURE create_forecast_generation_role(IN orgid BINARY(16))
102+
COMMENT 'Create a role that provides the necessary access to generate reference NWP or persistence forecast values'
103+
MODIFIES SQL DATA SQL SECURITY DEFINER
104+
BEGIN
105+
-- relying on default permissions already created on create organization
106+
DECLARE roleid BINARY(16);
107+
SET roleid = UUID_TO_BIN(UUID(), 1);
108+
INSERT INTO arbiter_data.roles (name, description, id, organization_id) VALUES (
109+
'Generate reference forecasts', 'Enable writing forecast values for automatic reference forecasts',
110+
roleid, orgid);
111+
INSERT INTO arbiter_data.role_permission_mapping (role_id, permission_id)
112+
SELECT roleid, id FROM arbiter_data.permissions WHERE applies_to_all AND organization_id = orgid
113+
AND description IN (
114+
'Read all sites', 'Read all forecasts', 'Read all probabilistic forecasts', 'Read all aggregates',
115+
-- for persistence
116+
'Read all observations', 'Read all observation values', 'Read all aggregate values',
117+
'Submit values to all forecasts', 'Submit values to all probabilistic forecasts');
118+
SELECT BIN_TO_UUID(roleid, 1);
119+
END;
120+
121+
GRANT EXECUTE ON PROCEDURE arbiter_data.create_forecast_generation_role TO 'insert_rbac'@'localhost';
122+
GRANT EXECUTE ON PROCEDURE arbiter_data.create_forecast_generation_role TO 'frameworkadmin'@'%';
123+
124+
125+
CREATE DEFINER = 'insert_rbac'@'localhost' PROCEDURE grant_job_role (IN userstrid CHAR(36), IN role_name VARCHAR(32))
126+
COMMENT 'Grant one of the background job roles to a user'
127+
MODIFIES SQL DATA SQL SECURITY DEFINER
128+
BEGIN
129+
DECLARE userid BINARY(16);
130+
DECLARE orgid BINARY(16);
131+
DECLARE roleid BINARY(16) DEFAULT NULL;
132+
SET userid = UUID_TO_BIN(userstrid, 1);
133+
SET orgid = get_object_organization(userid, 'users');
134+
SET roleid = (SELECT id FROM arbiter_data.roles WHERE organization_id = orgid AND name = role_name
135+
AND name in ('Create reports', 'Validate observations', 'Generate reference forecasts'));
136+
IF roleid IS NULL THEN
137+
IF role_name = 'Create reports' THEN
138+
CALL create_report_creation_role(orgid);
139+
ELSEIF role_name = 'Validate observations' THEN
140+
CALL create_data_validation_role(orgid);
141+
ELSEIF role_name = 'Generate reference forecasts' THEN
142+
CALL create_forecast_generation_role(orgid);
143+
ELSE
144+
SIGNAL SQLSTATE '42000' SET MESSAGE_TEXT = 'Cannot create and assign job role',
145+
MYSQL_ERRNO = 1142;
146+
END IF;
147+
SET roleid = (SELECT id FROM arbiter_data.roles WHERE organization_id = orgid AND name = role_name);
148+
END IF;
149+
INSERT INTO arbiter_data.user_role_mapping (user_id, role_id) VALUES (userid, roleid);
150+
END;
151+
152+
GRANT EXECUTE ON PROCEDURE arbiter_data.grant_job_role TO 'insert_rbac'@'localhost';
153+
GRANT SELECT(id, organization_id, name) ON arbiter_data.roles TO 'insert_rbac'@'localhost';
154+
GRANT EXECUTE ON PROCEDURE arbiter_data.grant_job_role TO 'frameworkadmin'@'%';

datastore/tests/test_admin_procedures.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,3 +682,180 @@ def test_set_org_accepted_tou_org_dne(dictcursor):
682682
dictcursor.callproc('set_org_accepted_tou', (str(bin_to_uuid(orgid)),))
683683
assert e.value.args[0] == 1305
684684
assert e.value.args[1] == "Organization does not exist"
685+
686+
687+
def test_store_token(dictcursor, new_user):
688+
user = new_user()
689+
dictcursor.callproc('store_token', (user['auth0_id'], 'testtoken'))
690+
dictcursor.execute('SELECT * FROM job_tokens')
691+
out = dictcursor.fetchall()
692+
assert len(out) == 1
693+
assert out[0]['id'] == user['id']
694+
assert out[0]['token'] == 'testtoken'
695+
696+
dictcursor.callproc('store_token', (user['auth0_id'], 'newtoken'))
697+
dictcursor.execute('SELECT * FROM job_tokens')
698+
out = dictcursor.fetchall()
699+
assert len(out) == 1
700+
assert out[0]['id'] == user['id']
701+
assert out[0]['token'] == 'newtoken'
702+
703+
704+
def test_create_job_user(cursor, new_organization):
705+
org = new_organization()
706+
orgid = bin_to_uuid(org['id'])
707+
auth0_id = 'auth0|testid'
708+
cursor.callproc('create_job_user', (auth0_id, orgid))
709+
user_id = cursor.fetchone()[0]
710+
cursor.execute(
711+
'SELECT 1 as one FROM users WHERE auth0_id = %s '
712+
'AND organization_id = UUID_TO_BIN(%s, 1)',
713+
(auth0_id, orgid))
714+
assert cursor.fetchall()[0]
715+
cursor.execute(
716+
'SELECT name FROM roles WHERE id IN'
717+
'(select role_id from user_role_mapping where '
718+
'user_id = UUID_TO_BIN(%s, 1))',
719+
(user_id,)
720+
)
721+
roles = [r[0] for r in cursor.fetchall()]
722+
assert len(roles) == 2
723+
assert 'Read Reference Data' in roles
724+
assert f'DEFAULT User role {user_id}' in roles
725+
726+
727+
def test_create_report_creation_role(dictcursor):
728+
dictcursor.callproc('create_organization', ('test_org',))
729+
dictcursor.execute('SELECT * FROM arbiter_data.organizations '
730+
'WHERE name = "test_org"')
731+
org = dictcursor.fetchone()
732+
orgid = org['id']
733+
734+
dictcursor.callproc('create_report_creation_role', (orgid,))
735+
dictcursor.execute(
736+
'SELECT * FROM roles WHERE name = "Create reports" and organization_id = %s', # NOQA
737+
orgid)
738+
create_roles = dictcursor.fetchall()
739+
assert len(create_roles) == 1
740+
create_role = create_roles[0]
741+
assert 'Create reports' in create_role['description']
742+
assert create_role['organization_id'] == orgid
743+
role_id = create_role['id']
744+
dictcursor.execute(
745+
'SELECT permission_id FROM role_permission_mapping WHERE role_id = %s',
746+
role_id)
747+
permission_ids = dictcursor.fetchall()
748+
assert len(permission_ids) == 10
749+
perm_objects = []
750+
for permid in [p['permission_id'] for p in permission_ids]:
751+
dictcursor.execute('SELECT * FROM permissions WHERE id = %s', permid)
752+
perm = dictcursor.fetchone()
753+
perm_objects.append(perm)
754+
perms = {p['description']: p for p in perm_objects}
755+
for perm in perms.values():
756+
assert perm['applies_to_all'] == 1
757+
assert perm['organization_id'] == orgid
758+
assert {'Read all sites', 'Read all observations',
759+
'Read all observation values', 'Read all forecasts',
760+
'Read all forecast values', 'Read all probabilistic forecasts',
761+
'Read all probabilistic forecast values',
762+
'Read all aggregates', 'Read all aggregate values',
763+
'Create reports'} == set(perms.keys())
764+
765+
766+
def test_create_data_validation_role(dictcursor):
767+
dictcursor.callproc('create_organization', ('test_org',))
768+
dictcursor.execute('SELECT * FROM arbiter_data.organizations '
769+
'WHERE name = "test_org"')
770+
org = dictcursor.fetchone()
771+
orgid = org['id']
772+
773+
dictcursor.callproc('create_data_validation_role', (orgid,))
774+
dictcursor.execute(
775+
'SELECT * FROM roles WHERE name = "Validate observations" and organization_id = %s', # NOQA
776+
orgid)
777+
create_roles = dictcursor.fetchall()
778+
assert len(create_roles) == 1
779+
create_role = create_roles[0]
780+
assert 'Enable observation data validation' in create_role['description']
781+
assert create_role['organization_id'] == orgid
782+
role_id = create_role['id']
783+
dictcursor.execute(
784+
'SELECT permission_id FROM role_permission_mapping WHERE role_id = %s',
785+
role_id)
786+
permission_ids = dictcursor.fetchall()
787+
assert len(permission_ids) == 4
788+
perm_objects = []
789+
for permid in [p['permission_id'] for p in permission_ids]:
790+
dictcursor.execute('SELECT * FROM permissions WHERE id = %s', permid)
791+
perm = dictcursor.fetchone()
792+
perm_objects.append(perm)
793+
perms = {p['description']: p for p in perm_objects}
794+
for perm in perms.values():
795+
assert perm['applies_to_all'] == 1
796+
assert perm['organization_id'] == orgid
797+
assert {'Read all sites', 'Read all observations', 'Read all observation values',
798+
'Submit values to all observations'} == set(perms.keys())
799+
800+
801+
def test_create_forecast_generation_role(dictcursor):
802+
dictcursor.callproc('create_organization', ('test_org',))
803+
dictcursor.execute('SELECT * FROM arbiter_data.organizations '
804+
'WHERE name = "test_org"')
805+
org = dictcursor.fetchone()
806+
orgid = org['id']
807+
808+
dictcursor.callproc('create_forecast_generation_role', (orgid,))
809+
dictcursor.execute(
810+
'SELECT * FROM roles WHERE name = "Generate reference forecasts" and organization_id = %s', # NOQA
811+
orgid)
812+
create_roles = dictcursor.fetchall()
813+
assert len(create_roles) == 1
814+
create_role = create_roles[0]
815+
assert 'Enable writing forecast values for ' in create_role['description']
816+
assert create_role['organization_id'] == orgid
817+
role_id = create_role['id']
818+
dictcursor.execute(
819+
'SELECT permission_id FROM role_permission_mapping WHERE role_id = %s',
820+
role_id)
821+
permission_ids = dictcursor.fetchall()
822+
assert len(permission_ids) == 9
823+
perm_objects = []
824+
for permid in [p['permission_id'] for p in permission_ids]:
825+
dictcursor.execute('SELECT * FROM permissions WHERE id = %s', permid)
826+
perm = dictcursor.fetchone()
827+
perm_objects.append(perm)
828+
perms = {p['description']: p for p in perm_objects}
829+
for perm in perms.values():
830+
assert perm['applies_to_all'] == 1
831+
assert perm['organization_id'] == orgid
832+
833+
assert {'Read all sites', 'Read all forecasts',
834+
'Read all probabilistic forecasts', 'Read all aggregates',
835+
'Read all observations', 'Read all observation values',
836+
'Read all aggregate values',
837+
'Submit values to all forecasts',
838+
'Submit values to all probabilistic forecasts'} == set(perms.keys())
839+
840+
841+
@pytest.mark.parametrize('role,precreate', [
842+
('Create reports', None),
843+
('Validate observations', None),
844+
('Generate reference forecasts', None),
845+
pytest.param('Read all', None, marks=pytest.mark.xfail),
846+
('Create reports', 'create_report_creation_role'),
847+
('Validate observations', 'create_report_creation_role'),
848+
('Validate observations', 'create_data_validation_role')
849+
])
850+
def test_grant_job_role(cursor, new_user, role, precreate):
851+
user = new_user()
852+
struserid = str(bin_to_uuid(user['id']))
853+
if precreate is not None:
854+
cursor.callproc(precreate, (user['organization_id'],))
855+
856+
cursor.callproc('grant_job_role', (struserid, role))
857+
cursor.execute(
858+
'SELECT 1 FROM user_role_mapping WHERE user_id = %s AND role_id = '
859+
'(SELECT id FROM roles WHERE name = %s AND organization_id = %s)',
860+
(user['id'], role, user['organization_id']))
861+
assert cursor.fetchone()[0]

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
'pymysql',
4848
'solarforecastarbiter',
4949
'sentry_sdk',
50-
'blinker'
50+
'blinker',
51+
'cryptography'
5152
],
5253
extras_require=EXTRAS_REQUIRE,
5354
project_urls={

0 commit comments

Comments
 (0)