From f3b48d0678fe23c8ff4aed8bfdc370b3b8197b9f Mon Sep 17 00:00:00 2001 From: Jeffrey Armstrong Date: Wed, 4 May 2022 12:08:32 -0400 Subject: Auth levels are now read from a special config file. Both display and operations are now checked for auth level. --- captain/config.f90 | 92 +++++++++++ captain/db.f90 | 3 +- captain/example/levitating-permissions.conf | 14 ++ captain/example/levitating.conf | 27 ++-- captain/http.f90 | 1 + captain/levitating-captain.prj | 3 + captain/requtils.f90 | 54 +++++-- captain/response.f90 | 12 ++ captain/web.f90 | 226 +++++++++++++++++----------- 9 files changed, 321 insertions(+), 111 deletions(-) create mode 100644 captain/example/levitating-permissions.conf diff --git a/captain/config.f90 b/captain/config.f90 index 509c489..2d67adf 100644 --- a/captain/config.f90 +++ b/captain/config.f90 @@ -71,6 +71,24 @@ implicit none character(*), parameter::SALT_VARIABLE = "security-salt" character(1024)::app_salt + character(*), parameter::PERMISSIONS_FILE_VARIABLE = "permissions_file" + character(1024)::perm_filename + + integer, parameter::MAX_PERMISSIONS = 16 + + type::permissions + character(len=40), dimension(MAX_PERMISSIONS)::k + integer, dimension(MAX_PERMISSIONS)::v + + contains + + procedure :: load => load_permissions + procedure :: get => get_permission + + end type permissions + + type(permissions)::global_permissions + contains subroutine get_variable(str, v) @@ -162,6 +180,9 @@ contains else if(cvariable == SALT_VARIABLE) then app_salt = trim(cvalue) + + else if(cvariable == PERMISSIONS_FILE_VARIABLE) then + perm_filename = trim(cvalue) end if @@ -174,6 +195,7 @@ contains integer::unit_number, istatus character(1024)::line, cvalue character(64)::cvariable + integer::i open(newunit=unit_number, file=trim(filename), status='old', & action="read", iostat=istatus) @@ -195,6 +217,16 @@ contains close(unit_number) + if(len_trim(perm_filename) > 0) then + if(perm_filename(1:1) /= "/") then + i = index(filename, "/", back=.true.) + if(i > 1) then + perm_filename = filename(1:i)//trim(perm_filename) + end if + end if + call global_permissions%load(perm_filename) + end if + end subroutine load_configuration subroutine template_filepath(x, res) @@ -207,5 +239,65 @@ contains call combine_paths(template_directory, x, res) end subroutine template_filepath + + subroutine load_permissions(self, filename) + implicit none + + class(permissions), intent(out)::self + character(*), intent(in)::filename + + integer::unit_number, istatus + character(1024)::line, cvalue + character(64)::cvariable + integer::i + + self%k = " " + + open(newunit=unit_number, file=trim(filename), status='old', & + action="read", iostat=istatus) + + i = 1 + + read(unit_number, '(A)', iostat=istatus) line + do while(istatus == 0 .and. i <= MAX_PERMISSIONS) + + if(len_trim(line) > 0 .and. line(1:1) /= '#') then + + call get_variable(line, cvariable) + call get_value(line, cvalue) + + self%k(i) = cvariable + read(cvalue, *, iostat=istatus) self%v(i) + + i = i + 1 + end if + + read(unit_number, '(A)', iostat=istatus) line + end do + + close(unit_number) + + end subroutine load_permissions + + pure function get_permission(self, key) + use auth_levels, only: AUTH_ADMIN_USER + implicit none + + class(permissions), intent(in)::self + character(len=*), intent(in)::key + integer::get_permission + + integer::i + + get_permission = AUTH_ADMIN_USER + + do i = 1, MAX_PERMISSIONS + if(trim(self%k(i)) == trim(key)) then + get_permission = self%v(i) + exit + end if + end do + + end function get_permission end module config diff --git a/captain/db.f90 b/captain/db.f90 index 97c397a..44bc473 100644 --- a/captain/db.f90 +++ b/captain/db.f90 @@ -1541,6 +1541,7 @@ contains end function get_user_auth_db function get_session_auth_db(session) + use auth_levels, only: AUTH_NONE implicit none character(len=*), intent(in)::session @@ -1548,7 +1549,7 @@ contains type(sqlite3_stmt)::stmt - get_session_auth_db = -1 + get_session_auth_db = AUTH_NONE if(stmt%prepare(db, "SELECT level FROM session_auth WHERE session=? LIMIT 1") == SQLITE_OK) then if(stmt%bind_text(1, session) == SQLITE_OK) then diff --git a/captain/example/levitating-permissions.conf b/captain/example/levitating-permissions.conf new file mode 100644 index 0000000..a487872 --- /dev/null +++ b/captain/example/levitating-permissions.conf @@ -0,0 +1,14 @@ + +access-releases = 0 +access-logs = 0 + +add-players = 10 +modify-players = 5 + +scan-instructions = 10 +launch-job = 5 +assign-instructions = 5 +view-raw-instructions = 0 + +add-groups = 5 +modify-groups = 5 diff --git a/captain/example/levitating.conf b/captain/example/levitating.conf index c1c22e1..06b89ed 100644 --- a/captain/example/levitating.conf +++ b/captain/example/levitating.conf @@ -1,9 +1,9 @@ -template-directory = /home/jeff/Workspace/levitating/captain/templates +template-directory = /home/jeff/workspace/levitating/captain/templates -database = /home/jeff/Workspace/levitating/captain/example/store.db +database = /home/jeff/workspace/levitating/captain/example/store.db -log-filename = /home/jeff/Workspace/levitating/captain/example/log/levitating.log +log-filename = /home/jeff/workspace/levitating/captain/example/log/levitating.log log-level = 10 @@ -11,22 +11,25 @@ project = misc-build description = A builder for stuff -public-cert = /home/jeff/Workspace/levitating/captain/example/pub.crt +public-cert = /home/jeff/workspace/levitating/captain/example/pub.crt -private-cert = /home/jeff/Workspace/levitating/captain/example/priv.key +private-cert = /home/jeff/workspace/levitating/captain/example/priv.key -uploads-directory = /home/jeff/Workspace/levitating/captain/example/uploads +uploads-directory = /home/jeff/workspace/levitating/captain/example/uploads -results-directory = /home/jeff/Workspace/levitating/captain/example/results +results-directory = /home/jeff/workspace/levitating/captain/example/results -static-directory = /home/jeff/Workspace/levitating/captain/example/static +static-directory = /home/jeff/workspace/levitating/captain/example/static -script-directory = /home/jeff/Workspace/levitating/captain/sql +script-directory = /home/jeff/workspace/levitating/captain/sql -instructions-directory = /home/jeff/Workspace/levitating/captain/example/instructions +instructions-directory = /home/jeff/workspace/levitating/captain/example/instructions -release-directory = /home/jeff/Workspace/levitating/captain/example/releases +release-directory = /home/jeff/workspace/levitating/captain/example/releases temp-directory = /tmp/levitating -security-salt = aBcD____ \ No newline at end of file +security-salt = aBcD____ + +# Must be absolute path or relative to this file +permissions_file = levitating-permissions.conf diff --git a/captain/http.f90 b/captain/http.f90 index 9e2fca5..8449ba2 100644 --- a/captain/http.f90 +++ b/captain/http.f90 @@ -27,6 +27,7 @@ implicit none integer, parameter::HTTP_CODE_NOTFOUND = 404 integer, parameter::HTTP_CODE_FAILURE = 500 integer, parameter::HTTP_CODE_REDIRECT = 302 + integer, parameter::HTTP_CODE_UNAUTHORIZED = 401 contains diff --git a/captain/levitating-captain.prj b/captain/levitating-captain.prj index 4af03c5..f949fb2 100644 --- a/captain/levitating-captain.prj +++ b/captain/levitating-captain.prj @@ -49,6 +49,9 @@ }], "Name":"+example", "Files":[{ + "filename":"example/levitating-permissions.conf", + "enabled":"1" + },{ "filename":"example/levitating.conf", "enabled":"1" }] diff --git a/captain/requtils.f90 b/captain/requtils.f90 index 41eacc6..620170a 100644 --- a/captain/requtils.f90 +++ b/captain/requtils.f90 @@ -71,6 +71,24 @@ contains end if end function notfound_code + + pure function notpermitted_code(req) + use http, only: HTTP_UNAUTHORIZED => HTTP_CODE_UNAUTHORIZED + use server_response, only: request, GEMINI_UNAUTHORIZED => GEMINI_CODE_BAD_REQUEST + implicit none + + class(request), intent(in)::req + integer::notpermitted_code + + if(req%protocol == 'gemini') then + ! You might think we'd use Gemini certificates, but fuck certificates... + ! Just fail with a bad request. + notpermitted_code = GEMINI_UNAUTHORIZED + else + notpermitted_code = HTTP_UNAUTHORIZED + end if + + end function notpermitted_code subroutine basic_mimetype(actual_filename, mimetype) use utilities, only: get_one_line_output_shell_command @@ -239,19 +257,30 @@ contains call req%path_component(1, category) call req%path_starting_with_component(2, filename) - resp%body_filename => get_special_full_filename(trim(category), trim(filename)) - - inquire(file=resp%body_filename, exist=exists) - if(.not. exists) then - - resp%code = notfound_code(req) - call write_log("File did not exist: "//resp%body_filename, LOG_NORMAL) + if((req%auth_level < global_permissions%get("view-raw-instructions") .and. trim(category) == "instructions") .or. & + (req%auth_level < global_permissions%get("access-releases") .and. trim(category) == "releases") .or. & + (req%auth_level < global_permissions%get("access-logs") .and. trim(category) == "results")) & + then + resp%code = notpermitted_code(req) + else + + resp%body_filename => get_special_full_filename(trim(category), trim(filename)) + + inquire(file=resp%body_filename, exist=exists) + if(.not. exists) then - resp%code = success_code(req) - call basic_mimetype(resp%body_filename, resp%body_mimetype) + resp%code = notfound_code(req) + call write_log("File did not exist: "//resp%body_filename, LOG_NORMAL) + + else + + resp%code = success_code(req) + call basic_mimetype(resp%body_filename, resp%body_mimetype) + end if + end if end function request_static @@ -652,6 +681,7 @@ contains use captain_db use server_response use remote_launch + use config, only: global_permissions implicit none type(request), intent(in)::req @@ -668,15 +698,15 @@ contains command = req%query_string(1:i-1) argument = req%query_string(i+1:len_trim(req%query_string)) - if(trim(command) == "launch") then + if(trim(command) == "launch" .and. req%auth_level >= global_permissions%get("launch-job")) then call launch_instructions_on_player(instruction_name, argument) - else if(trim(command) == "assign") then + else if(trim(command) == "assign" .and. req%auth_level >= global_permissions%get("assign-instructions")) then i = get_instruction_id(trim(instruction_name)) j = get_player_id(trim(argument)) call add_player_for_instruction(i, j) - else if(trim(command) == "remove") then + else if(trim(command) == "remove" .and. req%auth_level >= global_permissions%get("assign-instructions")) then i = get_instruction_id(trim(instruction_name)) j = get_player_id(trim(argument)) call remove_player_for_instruction(i, j) diff --git a/captain/response.f90 b/captain/response.f90 index 34c537c..e172cc1 100644 --- a/captain/response.f90 +++ b/captain/response.f90 @@ -73,6 +73,8 @@ implicit none character(len=:), pointer::token => null() character(len=4)::method = "GET" + integer::auth_level + type(query)::q type(cookies)::c @@ -111,6 +113,8 @@ contains subroutine request_init(self, str, server_explicit, protocol_explicit, method, cookiestring) use logging use utilities, only: toupper + use captain_db, only: get_session_auth_db + use auth_levels, only: AUTH_NONE implicit none class(request) :: self @@ -222,6 +226,14 @@ contains if(.not.associated(self%token) .and. associated(self%c%get_value("token"))) then self%token => self%c%get_value("token") end if + else + call self%c%init() + end if + + if(associated(self%token)) then + self%auth_level = get_session_auth_db(self%token) + else + self%auth_level = AUTH_NONE end if end subroutine request_init diff --git a/captain/web.f90 b/captain/web.f90 index b990c1a..9d8fc3c 100644 --- a/captain/web.f90 +++ b/captain/web.f90 @@ -70,6 +70,17 @@ contains end function request_is_authenticated + subroutine echo_error_stdout(error_code) + implicit none + + integer, intent(in)::error_code + + Print *, "Falling...

An Error Occurred:" + Print *, error_code + Print *, "

" + + end subroutine echo_error_stdout + subroutine handle_basic_template_components(req, page) use server_response use page_template @@ -151,6 +162,7 @@ contains use captain_db use server_response use request_utils, only: get_player_status_utf8, render_jobs_links, generate_simple_pager + use config, only: global_permissions implicit none type(request)::req @@ -219,14 +231,17 @@ contains res = "

"//trim(instruction_name)//"

" - one_link => html_link(trim(instruction_name)//".json", & - "View Raw") - res = trim(res)//nl//"

"//one_link//"

" - deallocate(one_link) + if(req%auth_level >= global_permissions%get("view-raw-instructions")) then + one_link => html_link(trim(instruction_name)//".json", & + "View Raw") + res = trim(res)//nl//"

"//one_link//"

" + deallocate(one_link) + end if if(n_players == 0) then res = trim(res)//nl//"

No players currently can run these instructions

" - else + + else if(req%auth_level >= global_permissions%get("launch-job")) then res = trim(res)//nl//"

Launch Now

"//nl//"