From 1bc8b16c472d4bc12dea09a8f69837b99f4d047b Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Fri, 3 Apr 2026 15:33:31 -0700 Subject: [PATCH 1/2] update at_api_keys table to allow for a key_id column this will allow for rapid lookup via CDA when attempting to check secret_hash also rename apikey column to secret_hash in order to explicitly call out apikeys being hashed. update secret hash size to 512 to be well over the bounds of argon2 hashing. Update to only allow moving the EXPIRES backwards as we do not want to be able to resurrect expired keys --- schema/src/cwms/tables/at_api_keys.sql | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/schema/src/cwms/tables/at_api_keys.sql b/schema/src/cwms/tables/at_api_keys.sql index 6a85d637..a45b3c73 100644 --- a/schema/src/cwms/tables/at_api_keys.sql +++ b/schema/src/cwms/tables/at_api_keys.sql @@ -1,25 +1,26 @@ create table at_api_keys( + key_id raw(16) not null, userid varchar2(128) not null references at_sec_cwms_users(USERID), key_name varchar2(64) not null, - apikey varchar2(256) not null unique, - created date default current_timestamp not null, - expires date default current_timestamp+1, - primary key(userid,key_name) + secret_hash varchar2(512) not null, + created timestamp default current_timestamp not null, + expires timestamp default current_timestamp+1, + constraint at_api_keys_pk primary key (key_id), + constraint at_api_keys_u1 unique (userid, key_name) ); -comment on column at_api_keys.apikey is - 'While randomly generated, these still have to be unique. Applications generating them should check and provide a different value'; - create or replace trigger cwms_20.st_api_key_readonly before update on cwms_20.at_api_keys for each row begin - if( :new.userid <> :old.userid + if( (:new.userid <> :old.userid OR :new.key_name <> :old.key_name - OR :new.apikey <> :old.apikey + OR :new.secret_hash <> :old.secret_hash OR :new.created <> :old.created + OR :new.key_id <> :old.key_id) + AND :new.expires < :old.expires ) then - raise_application_error(-20001,'API Key table is unmodifiabled except to update expiration or by deletion.'); + raise_application_error(-20001,'Only EXPIRES may be updated; all other columns are immutable.'); end if; end; / From 773b77a50c8c7a2232873fae87010a4891324f19 Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Fri, 3 Apr 2026 15:56:46 -0700 Subject: [PATCH 2/2] remove set_session_user_apikey as CDA uses set_session_user_direct fix apikey view fix unit test on at_api_keys --- schema/src/cwms/cwms_env_pkg.sql | 9 ----- schema/src/cwms/cwms_env_pkg_body.sql | 8 ----- schema/src/cwms/views/av_active_api_keys.sql | 3 +- schema/src/test/test_webuser_abilities.sql | 36 +++----------------- 4 files changed, 6 insertions(+), 50 deletions(-) diff --git a/schema/src/cwms/cwms_env_pkg.sql b/schema/src/cwms/cwms_env_pkg.sql index f0a8d38b..183cec69 100644 --- a/schema/src/cwms/cwms_env_pkg.sql +++ b/schema/src/cwms/cwms_env_pkg.sql @@ -21,15 +21,6 @@ AS * @parameter p_office The office for a particular session. */ PROCEDURE set_session_user_direct(p_user VARCHAR2, p_office VARCHAR2 default NULL); - - /** - * For user by WEB_USER accounts, set the role given the provided apikey - * - * @parameter p_apikey A user authorization token with a user defined (default 1 day) lifetime. - * @parameter p_office The office required for a particular session. - * - */ - PROCEDURE set_session_user_apikey(p_apikey VARCHAR2, p_office VARCHAR2 default NULL); END cwms_env; / diff --git a/schema/src/cwms/cwms_env_pkg_body.sql b/schema/src/cwms/cwms_env_pkg_body.sql index 7c50a956..ea628817 100644 --- a/schema/src/cwms/cwms_env_pkg_body.sql +++ b/schema/src/cwms/cwms_env_pkg_body.sql @@ -166,14 +166,6 @@ AS raise; END set_session_user_direct; - PROCEDURE set_session_user_apikey(p_apikey VARCHAR2, p_office VARCHAR2) - IS - l_userid at_sec_cwms_users.userid%type := null; - BEGIN - select userid into l_userid from cwms_20.av_active_api_keys where apikey = p_apikey; - set_session_user_direct(l_userid,p_office); - end set_session_user_apikey; - PROCEDURE set_session_privileges IS l_office_id VARCHAR2 (16); diff --git a/schema/src/cwms/views/av_active_api_keys.sql b/schema/src/cwms/views/av_active_api_keys.sql index 5aa422bd..fb51e854 100644 --- a/schema/src/cwms/views/av_active_api_keys.sql +++ b/schema/src/cwms/views/av_active_api_keys.sql @@ -1,8 +1,9 @@ create or replace view av_active_api_keys as select + key_id, userid, key_name, - apikey, + secret_hash, created, expires from diff --git a/schema/src/test/test_webuser_abilities.sql b/schema/src/test/test_webuser_abilities.sql index 000f84ee..95661236 100644 --- a/schema/src/test/test_webuser_abilities.sql +++ b/schema/src/test/test_webuser_abilities.sql @@ -118,45 +118,17 @@ AS procedure can_interact_with_api_keys_table_and_view is l_userid varchar(128) := upper('&eroc.hectest'); - l_testkey cwms_20.at_api_keys.apikey%type := 'A simple test key'; + l_testkey cwms_20.at_api_keys.secret_hash%type := 'A simple test key'; l_testkey_name cwms_20.at_api_keys.key_name%type := 'A test key'; l_testkey_name_out cwms_20.at_api_keys.key_name%type; begin cwms_20.cwms_env.set_session_user_direct('&&eroc.webtest','&&office_id'); - insert into cwms_20.at_api_keys(userid,key_name,apikey) - values (l_userid,l_testkey_name,l_testkey); - select key_name into l_testkey_name_out from cwms_20.av_active_api_keys where apikey=l_testkey; + insert into cwms_20.at_api_keys(key_id,userid,key_name,secret_hash) + values (secret_hash, l_userid,l_testkey_name,l_testkey); + select key_name into l_testkey_name_out from cwms_20.av_active_api_keys where secret_hash=l_testkey; ut.expect(l_testkey_name_out).to_equal(l_testkey_name); end; - - procedure can_set_context_user_by_key is - l_testkey1 cwms_20.at_api_keys.apikey%type := 'User 1 Test Key'; - l_testkey2 cwms_20.at_api_keys.apikey%type := 'User 2 Test Key'; - l_user1 varchar2(128) := upper('&eroc.hectest'); - l_user2 varchar2(128) := upper('&eroc.hectest_ro'); - l_priv varchar2(255); - begin - insert into cwms_20.at_api_keys(userid,key_name,apikey) - values (l_user1,l_testkey1,l_testkey1); - insert into cwms_20.at_api_keys(userid,key_name,apikey) - values (l_user2,l_testkey2,l_testkey2); - - cwms_env.set_session_user_apikey(l_testkey1,'&&office_id'); - ut.expect(cwms_util.get_user_id).to_equal(l_user1); - - -- test without office ID set - cwms_env.set_session_user_apikey(l_testkey2); - ut.expect(cwms_util.get_user_id).to_equal(l_user2); - ut.expect(SYS_CONTEXT ('CWMS_ENV', 'CWMS_PRIVILEGE')).to_equal('READ_ONLY'); - - /** I don't believe it but this actually is required, which is good. But - we should still call it to make sure it doesn't fail. - */ - cwms_env.set_session_user_direct(upper('&eroc.webtest'),'&&office_id'); - - end; - PROCEDURE test_create_cwms_cwbi_user IS l_count number;