Compare commits

..

No commits in common. "4f98a4834ebffe1c5f1f45961ffb1551257799bc" and "f4576cf7ea3315627ceb58c07c205e13d4ea410d" have entirely different histories.

12 changed files with 40 additions and 226 deletions

3
.gitignore vendored
View File

@ -6,9 +6,6 @@ test/.pt.db
*compile_commands.json *compile_commands.json
test/perf* test/perf*
test/callgraph* test/callgraph*
test/openers
test/opener/opener
test/opener/opener.o
src/gui/ui/* src/gui/ui/*
src/gui/*.o src/gui/*.o
src/gui/icfs_dialogue src/gui/icfs_dialogue

View File

@ -1,6 +1,6 @@
#ifndef ACCESS_T_H #ifndef ACCESS_T_H
#define ACCESS_T_H #define ACCESS_T_H
typedef enum { DENY, ALLOW, ALLOW_TEMP, DENY_TEMP, NDEF } access_t; typedef enum { DENY, ALLOW, ALLOW_TEMP, NDEF } access_t;
#endif // !ACCESS_T_H #endif // !ACCESS_T_H

View File

@ -138,13 +138,8 @@ static int on_command_line(GApplication *app, GApplicationCommandLine *cmdline,
} }
int main(int argc, char **argv) { int main(int argc, char **argv) {
if (argc == 2 && strcmp(argv[1], "--version") == 0) {
fprintf(stdout, "icfs_dialogue 1.0.0");
}
// Create a new application // Create a new application
AdwApplication *app = adw_application_new("de.umbrasolis.icfs_dialogue", AdwApplication *app = adw_application_new("com.example.zenityclone",
G_APPLICATION_HANDLES_COMMAND_LINE); G_APPLICATION_HANDLES_COMMAND_LINE);
g_signal_connect(app, "command-line", G_CALLBACK(on_command_line), NULL); g_signal_connect(app, "command-line", G_CALLBACK(on_command_line), NULL);

View File

@ -9,7 +9,6 @@
#include "perm_permissions_table.h" #include "perm_permissions_table.h"
#include "access_t.h" #include "access_t.h"
#include "process_info.h" #include "process_info.h"
#include "set_mode_t.h"
#include <fcntl.h> #include <fcntl.h>
#include <pthread.h> #include <pthread.h>
#include <sqlite3.h> #include <sqlite3.h>
@ -84,21 +83,10 @@ static int check_table_col_schema(void *notused, int argc, char **argv,
} }
static int set_flag(void *flag, int argc, char **argv, char **colname) { static int set_flag(void *flag, int argc, char **argv, char **colname) {
(void)argc;
(void)argv;
(void)colname; (void)colname;
*(int *)flag = 1;
if (argc < 3) {
fprintf(stderr,
"Unexpected amount of arguments given to the callback: %d.\n",
argc);
return 1;
}
if (atoi(argv[2])) {
fprintf(stderr, "Third column was: %s\n", argv[2]);
*(int *)flag = 1;
} else {
*(int *)flag = -1;
}
return 0; return 0;
} }
@ -194,7 +182,7 @@ int init_perm_permissions_table(const char *db_filename) {
/** /**
* Destroys the permanent permissions table. * Destroys the permanent permissions table.
*/ */
void destroy_perm_permissions_table(void) { sqlite3_close(perm_database); } void destroy_perm_permissions_table() { sqlite3_close(perm_database); }
/** /**
* Checks if the process has a permanent access to the file. * Checks if the process has a permanent access to the file.
@ -209,7 +197,7 @@ access_t check_perm_access(const char *filename, struct process_info pi) {
char *query = NULL; char *query = NULL;
int ret = asprintf(&query, int ret = asprintf(&query,
"SELECT * FROM %s WHERE executable = \'%s\' " "SELECT * FROM %s WHERE executable = \'%s\' "
"AND filename = \'%s\';", "AND filename = \'%s\' AND mode = TRUE;",
table_name, pi.name, filename); table_name, pi.name, filename);
if (ret < 0) { if (ret < 0) {
@ -232,12 +220,9 @@ access_t check_perm_access(const char *filename, struct process_info pi) {
return NDEF; return NDEF;
} }
if (flag == 1) { if (flag) {
return ALLOW; return ALLOW;
} }
if (flag == -1) {
return DENY;
}
return NDEF; return NDEF;
} }
@ -248,19 +233,10 @@ access_t check_perm_access(const char *filename, struct process_info pi) {
* @param pi: The process information * @param pi: The process information
* @return: 0 on success, 1 on failure * @return: 0 on success, 1 on failure
*/ */
int set_perm_access(const char *filename, struct process_info pi, int give_perm_access(const char *filename, struct process_info pi) {
set_mode_t mode) {
char *query = NULL; char *query = NULL;
int ret = -1; int ret = asprintf(&query, "INSERT INTO %s VALUES (\'%s\', \'%s\', TRUE);",
if (mode == SET_ALLOW) { table_name, pi.name, filename);
ret = asprintf(&query, "INSERT INTO %s VALUES (\'%s\', \'%s\', TRUE);",
table_name, pi.name, filename);
} else if (mode == SET_DENY) {
ret = asprintf(&query, "INSERT INTO %s VALUES (\'%s\', \'%s\', FALSE);",
table_name, pi.name, filename);
} else {
return 1;
}
if (ret < 0) { if (ret < 0) {
// If asprintf fails, the contents of query are undefined (see man // If asprintf fails, the contents of query are undefined (see man

View File

@ -11,7 +11,6 @@
#include "access_t.h" #include "access_t.h"
#include "process_info.h" #include "process_info.h"
#include "set_mode_t.h"
/** /**
* Initializes the permanent permissions table. * Initializes the permanent permissions table.
@ -41,11 +40,8 @@ access_t check_perm_access(const char *filename, struct process_info pi);
* *
* @param filename: The file that the process is trying to access * @param filename: The file that the process is trying to access
* @param pi: The process information * @param pi: The process information
* @param mode: Kind of access rule to be set - SET_DENY to deny access, and
* SET_ALLOW to allow access.
* @return: 0 on success, -1 on failure * @return: 0 on success, -1 on failure
*/ */
int set_perm_access(const char *filename, struct process_info pi, int give_perm_access(const char *filename, struct process_info pi);
set_mode_t mode);
#endif // #ifdef PERM_PERMISSION_TABLE_H #endif // #ifdef PERM_PERMISSION_TABLE_H

View File

@ -1,5 +0,0 @@
#ifndef SET_MODE_T_H
#define SET_MODE_T_H
typedef enum { SET_DENY, SET_ALLOW } set_mode_t;
#endif // !SET_MODE_T_H

View File

@ -4,7 +4,6 @@
#include "access_t.h" #include "access_t.h"
#include "process_info.h" #include "process_info.h"
#include "set_mode_t.h"
/** /**
* Initializes the temporary permissions table. * Initializes the temporary permissions table.
@ -36,6 +35,8 @@ void destroy_temp_permissions_table(void);
*/ */
access_t check_temp_access(const char *filename, struct process_info pi); access_t check_temp_access(const char *filename, struct process_info pi);
typedef enum { SET_DENY, SET_ALLOW } set_mode_t;
/** /**
* Sets temporary access mode of the process to the file. * Sets temporary access mode of the process to the file.
* *

View File

@ -49,7 +49,7 @@ int init_ui_socket(const char *perm_permissions_db_filename) {
} }
// Test if Zenity is installed (get version) // Test if Zenity is installed (get version)
fp = popen("icfs_dialogue --version", "r"); fp = popen("zenity --version", "r");
if (fp == NULL) { if (fp == NULL) {
perror("Pipe returned an error"); perror("Pipe returned an error");
return 1; return 1;
@ -77,7 +77,7 @@ struct dialogue_response ask_access(const char *filename,
struct process_info proc_info) { struct process_info proc_info) {
FILE *fp = NULL; FILE *fp = NULL;
char *command = NULL; char *command = NULL;
int ret = asprintf(&command, "icfs_dialogue \"%d\" \"%s\" \"%s\" \"%s\"", int ret = asprintf(&command, "zenity \"%d\" \"%s\" \"%s\" \"%s\"",
proc_info.PID, proc_info.name, get_mountpoint(), filename); proc_info.PID, proc_info.name, get_mountpoint(), filename);
struct dialogue_response response; struct dialogue_response response;
@ -142,10 +142,8 @@ struct dialogue_response ask_access(const char *filename,
response.decision = ALLOW; response.decision = ALLOW;
} else if (zenity_exit_code == ZENITY_YES) { } else if (zenity_exit_code == ZENITY_YES) {
response.decision = ALLOW_TEMP; response.decision = ALLOW_TEMP;
} else if (zenity_exit_code == (ZENITY_NO | ZENITY_PERM)) {
response.decision = DENY;
} else { } else {
response.decision = DENY_TEMP; response.decision = DENY;
} }
return response; return response;
@ -207,7 +205,7 @@ int interactive_access(const char *filename, struct process_info proc_info,
if (opts & GRANT_PERM) { if (opts & GRANT_PERM) {
fprintf(stderr, "Permission granted permanently to %s.\n", proc_info.name); fprintf(stderr, "Permission granted permanently to %s.\n", proc_info.name);
set_perm_access(real_path, proc_info, SET_ALLOW); give_perm_access(real_path, proc_info);
free(real_path); free(real_path);
return 1; return 1;
} }
@ -243,7 +241,7 @@ int interactive_access(const char *filename, struct process_info proc_info,
fprintf(stderr, fprintf(stderr,
"Permission granted permanently to %s based on zenty response.\n", "Permission granted permanently to %s based on zenty response.\n",
proc_info.name); proc_info.name);
set_perm_access(real_path, proc_info, SET_ALLOW); give_perm_access(real_path, proc_info);
free(real_path); free(real_path);
return 1; return 1;
} }
@ -257,7 +255,7 @@ int interactive_access(const char *filename, struct process_info proc_info,
return 1; return 1;
} }
if (response.decision == DENY_TEMP) { if (response.decision == DENY) {
fprintf(stderr, fprintf(stderr,
"Permission denied temporarily to %s based on zenty response.\n", "Permission denied temporarily to %s based on zenty response.\n",
proc_info.name); proc_info.name);
@ -266,15 +264,6 @@ int interactive_access(const char *filename, struct process_info proc_info,
return 0; return 0;
} }
if (response.decision == DENY) {
fprintf(stderr,
"Permission denied permanently to %s based on zenty response.\n",
proc_info.name);
set_perm_access(real_path, proc_info, SET_DENY);
free(real_path);
return 0;
}
free(real_path); free(real_path);
// deny on unknown options. // deny on unknown options.
return 0; return 0;

View File

@ -14,14 +14,12 @@ else
FAKE_ZENITY_RESPONSE=$(cat ~/.fake_zenity_response) FAKE_ZENITY_RESPONSE=$(cat ~/.fake_zenity_response)
printf "%s" "$4" printf "%s" "$4"
if [[ $FAKE_ZENITY_RESPONSE == "yes" ]]; then if [[ $FAKE_ZENITY_RESPONSE == "yes_tmp" ]]; then
exit "$ZENITY_YES" exit "$ZENITY_YES"
elif [[ $FAKE_ZENITY_RESPONSE == "no" ]]; then elif [[ $FAKE_ZENITY_RESPONSE == "no" ]]; then
exit "$ZENITY_NO" exit "$ZENITY_NO"
elif [[ $FAKE_ZENITY_RESPONSE == "yes_perm" ]]; then elif [[ $FAKE_ZENITY_RESPONSE == "yes" ]]; then
exit "$((ZENITY_YES | ZENITY_PERM))" exit "$((ZENITY_YES | ZENITY_PERM))"
elif [[ $FAKE_ZENITY_RESPONSE == "no_perm" ]]; then
exit "$((ZENITY_NO | ZENITY_PERM))"
fi fi
fi fi
fi fi

View File

@ -1,81 +0,0 @@
SHELL=/bin/bash
# configurable options
ifndef ($(SOURCES_DIR))
SOURCES_DIR := .
endif
ifndef ($(TESTS_DIR))
TESTS_DIR := .
endif
ifndef ($(BUILD_DIR))
BUILD_DIR := .
endif
CC := gcc
CXX := g++
NAME := opener
# dependencies
PACKAGE_NAMES :=
ifeq ($(TEST), 1)
# PACKAGE_NAMES += check # TODO: use check?
endif
# set up cflags and libs
CFLAGS :=
LDFLAGS :=
ifneq ($(PACKAGE_NAMES),)
CFLAGS += $(shell pkg-config --cflags $(PACKAGE_NAMES))
LDFLAGS += $(shell pkg-config --libs $(PACKAGE_NAMES))
endif
ifeq ($(DEBUG),1)
CFLAGS += -O0 -pedantic -g -Wall -Wextra -Wcast-align \
-Wcast-qual -Wdisabled-optimization -Wformat=2 \
-Winit-self -Wlogical-op -Wmissing-declarations \
-Wmissing-include-dirs -Wredundant-decls -Wshadow \
-Wsign-conversion -Wstrict-overflow=5 \
-Wswitch-default -Wundef -Wno-unused
LDFLAGS +=
else
CFLAGS += -O3
LDFLAGS +=
endif
# set up targets
TARGETS := $(BUILD_DIR)/$(NAME)
ifeq ($(TEST), 1)
TARGETS += $(NAME)_test
endif
# build!
default: $(TARGETS)
.PHONY: clean $(NAME)_test
$(NAME)_test: $(BUILD_DIR)/$(NAME)
echo "No tests defined."
#$(BUILD_DIR)/$(NAME)
$(BUILD_DIR)/$(NAME): $(BUILD_DIR)/$(NAME).o
$(CC) $(CFLAGS) $^ $(LDFLAGS) -o $(BUILD_DIR)/$(NAME)
$(BUILD_DIR)/$(NAME).o: $(SOURCES_DIR)/$(NAME).c
$(CC) $(CFLAGS) -c $< $(LDFLAGS) -o $(BUILD_DIR)/$(NAME).o
clean:
rm $(BUILD_DIR)/*.o $(BUILD_DIR)/$(NAME)

View File

@ -1,20 +0,0 @@
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: ./opener [FILENAME]");
return 1;
}
int fd = open(argv[1], O_RDWR | O_CREAT);
if (fd == -1) {
perror("Cannot open file");
return 1;
}
close(fd);
return 0;
}

View File

@ -9,17 +9,6 @@ touch ./protected/do-not-remove ./protected/should-be-removed ./protected/truth
chmod 777 ./protected/perm777 ./protected/perm000 chmod 777 ./protected/perm777 ./protected/perm000
echo "Free code, free world." >./protected/motto echo "Free code, free world." >./protected/motto
rm -rf ./openers
mkdir openers
make -C ./opener || (
echo "Could not make the opener program."
exit 1
)
for i in {1..10}; do
cp ./opener/opener "./openers/opener$i"
ln --symbolic "$(realpath "./openers/opener$i")" "./openers/symlinked_opener$i"
done
# set up the fake-zenity # set up the fake-zenity
PATH="$(realpath ./mock/):$PATH" PATH="$(realpath ./mock/):$PATH"
@ -39,7 +28,6 @@ if [[ $1 == "--setuid" ]]; then
else else
echo "Database protection will not be tested due to the lack of setuid capabilites." echo "Database protection will not be tested due to the lack of setuid capabilites."
echo "To test it, run this script with '--setuid'." echo "To test it, run this script with '--setuid'."
#valgrind --leak-check=full -s ../build/icfs -o default_permissions -o debug ./protected ./.pt.db 2>&1 | grep "==\|zenity\|Permission\|column\|callback" &
valgrind --leak-check=full -s ../build/icfs -o default_permissions ./protected ./.pt.db & valgrind --leak-check=full -s ../build/icfs -o default_permissions ./protected ./.pt.db &
sleep 5 sleep 5
fi fi
@ -51,105 +39,85 @@ fi
# create files # create files
icfs_dialogue --set-fake-response no zenity --set-fake-response no
truncate -s 0 ./protected/should-exist-anyway 2>/dev/null && truncate -s 0 ./protected/should-exist-anyway 2>/dev/null &&
echo "[ICFS-TEST]: OK" || echo "[ICFS-TEST]: OK" ||
echo "[ICFS-TEST]: truncate cannot create protected/should-exist despite access being permitted!" # OK echo "[ICFS-TEST]: truncate cannot create protected/should-exist despite access being permitted!" # OK
icfs_dialogue --set-fake-response yes zenity --set-fake-response yes_tmp
truncate -s 0 ./protected/should-exist 2>/dev/null && truncate -s 0 ./protected/should-exist 2>/dev/null &&
echo "[ICFS-TEST]: OK" || echo "[ICFS-TEST]: OK" ||
echo "[ICFS-TEST]: truncate cannot create protected/should-exist despite access being permitted!" # OK echo "[ICFS-TEST]: truncate cannot create protected/should-exist despite access being permitted!" # OK
# write to files # write to files
icfs_dialogue --set-fake-response no zenity --set-fake-response no
sed -e 'a\'"Linux is a cancer that attaches itself in an intellectual property sense to everything it touches." "./protected/truth" 2>/dev/null && sed -e 'a\'"Linux is a cancer that attaches itself in an intellectual property sense to everything it touches." "./protected/truth" 2>/dev/null &&
echo "[ICFS-TEST]: echo can write to protected/lie despite access being denied!" || echo "[ICFS-TEST]: echo can write to protected/lie despite access being denied!" ||
echo "[ICFS-TEST]: OK" # EACCESS echo "[ICFS-TEST]: OK" # EACCESS
icfs_dialogue --set-fake-response yes zenity --set-fake-response yes_tmp
sed -e 'a\'"Sharing knowledge is the most fundamental act of friendship. Because it is a way you can give something without loosing something." "./protected/truth" 2>/dev/null && sed -e 'a\'"Sharing knowledge is the most fundamental act of friendship. Because it is a way you can give something without loosing something." "./protected/truth" 2>/dev/null &&
echo "[ICFS-TEST]: OK" || echo "[ICFS-TEST]: OK" ||
echo "[ICFS-TEST]: echo cannot write to protected/truth despite access being permitted!" # OK echo "[ICFS-TEST]: echo cannot write to protected/truth despite access being permitted!" # OK
# Read files # Read files
icfs_dialogue --set-fake-response no zenity --set-fake-response no
cat ./protected/motto >/dev/null 2>/dev/null && cat ./protected/motto >/dev/null 2>/dev/null &&
echo "[ICFS-TEST]: cat can read protected/this-only despite access being denied!" || echo "[ICFS-TEST]: cat can read protected/this-only despite access being denied!" ||
echo "[ICFS-TEST]: OK" # EACCESS echo "[ICFS-TEST]: OK" # EACCESS
icfs_dialogue --set-fake-response yes zenity --set-fake-response yes_tmp
cat ./protected/motto >/dev/null 2>/dev/null && cat ./protected/motto >/dev/null 2>/dev/null &&
echo "[ICFS-TEST]: OK" || echo "[ICFS-TEST]: OK" ||
echo "[ICFS-TEST]: echo cannot create protected/this-only despite access being permitted!" # "Free code, free world." echo "[ICFS-TEST]: echo cannot create protected/this-only despite access being permitted!" # "Free code, free world."
# remove files # remove files
icfs_dialogue --set-fake-response no zenity --set-fake-response no
rm ./protected/do-not-remove >/dev/null 2>/dev/null && rm ./protected/do-not-remove >/dev/null 2>/dev/null &&
echo "[ICFS-TEST]: rm can unlink protected/do-not-remove despite access being denied!" || echo "[ICFS-TEST]: rm can unlink protected/do-not-remove despite access being denied!" ||
echo "[ICFS-TEST]: OK" # EACCESS echo "[ICFS-TEST]: OK" # EACCESS
icfs_dialogue --set-fake-response yes zenity --set-fake-response yes_tmp
rm ./protected/should-be-removed >/dev/null 2>/dev/null && rm ./protected/should-be-removed >/dev/null 2>/dev/null &&
echo "[ICFS-TEST]: OK" || echo "[ICFS-TEST]: OK" ||
echo "[ICFS-TEST]: rm cannot unlink protected/should-be-removed despite access being permitted!" # OK echo "[ICFS-TEST]: rm cannot unlink protected/should-be-removed despite access being permitted!" # OK
# rename files # rename files
icfs_dialogue --set-fake-response no zenity --set-fake-response no
mv ./protected/do-not-rename ./protected/terrible-name 2>/dev/null && mv ./protected/do-not-rename ./protected/terrible-name 2>/dev/null &&
echo "[ICFS-TEST]: mv can rename protected/truth despite access being denied!" || echo "[ICFS-TEST]: mv can rename protected/truth despite access being denied!" ||
echo "[ICFS-TEST]: OK" # EACCESS echo "[ICFS-TEST]: OK" # EACCESS
icfs_dialogue --set-fake-response yes zenity --set-fake-response yes_tmp
mv ./protected/should-be-renamed ./protected/great-name 2>/dev/null && mv ./protected/should-be-renamed ./protected/great-name 2>/dev/null &&
echo "[ICFS-TEST]: OK" || echo "[ICFS-TEST]: OK" ||
echo "[ICFS-TEST]: mv cannot rename should-be-removed to renamed-file despite access being permitted!" # OK echo "[ICFS-TEST]: mv cannot rename should-be-removed to renamed-file despite access being permitted!" # OK
# change permissions # change permissions
icfs_dialogue --set-fake-response no zenity --set-fake-response no
chmod 000 ./protected/perm777 2>/dev/null && chmod 000 ./protected/perm777 2>/dev/null &&
echo "[ICFS-TEST]: chmod can change permissions of protected/perm777 despite access being denied!" || echo "[ICFS-TEST]: chmod can change permissions of protected/perm777 despite access being denied!" ||
echo "[ICFS-TEST]: OK" # EACCESS echo "[ICFS-TEST]: OK" # EACCESS
icfs_dialogue --set-fake-response yes zenity --set-fake-response yes_tmp
chmod 000 ./protected/perm000 2>/dev/null && chmod 000 ./protected/perm000 2>/dev/null &&
echo "[ICFS-TEST]: OK" || echo "[ICFS-TEST]: OK" ||
echo "[ICFS-TEST]: chmod cannot change permissions of protected/perm000 despite access being permitted!" # OK echo "[ICFS-TEST]: chmod cannot change permissions of protected/perm000 despite access being permitted!" # OK
# test permanent permissions # test permanent permissions
icfs_dialogue --set-fake-response yes_perm zenity --set-fake-response yes
openers/opener1 ./protected/motto >/dev/null 2>/dev/null && cat ./protected/motto >/dev/null 2>/dev/null &&
echo "[ICFS-TEST]: OK" || echo "[ICFS-TEST]: OK" ||
echo "[ICFS-TEST]: openers/opener1 cannot read protected/motto despite access being permitted!" # OK echo "[ICFS-TEST]: echo cannot read protected/motto despite access being permitted!" # OK
icfs_dialogue --set-fake-response no # this should be ignored zenity --set-fake-response no # this should be ignored
openers/opener1 ./protected/motto >/dev/null 2>/dev/null && cat ./protected/motto >/dev/null 2>/dev/null &&
echo "[ICFS-TEST]: OK" || echo "[ICFS-TEST]: OK" ||
echo "[ICFS-TEST]: openers/opener1 cannot read protected/motto despite access being permitted!" # OK echo "[ICFS-TEST]: echo cannot read protected/motto despite access being permitted!" # OK
icfs_dialogue --set-fake-response no # this should be ignored
openers/symlinked_opener1 ./protected/motto >/dev/null 2>/dev/null &&
echo "[ICFS-TEST]: OK" ||
echo "[ICFS-TEST]: openers/symlinked_opener1 cannot read protected/motto despite access being permitted!" # OK
icfs_dialogue --set-fake-response no_perm
openers/opener2 ./protected/motto >/dev/null 2>/dev/null &&
echo "[ICFS-TEST]: openers/opener2 can read protected/motto despite access being denied!" ||
echo "[ICFS-TEST]: OK" # EACCESS
icfs_dialogue --set-fake-response yes # this should be ignored
openers/opener2 ./protected/motto >/dev/null 2>/dev/null &&
echo "[ICFS-TEST]: openers/opener2 can read protected/motto despite access being denied!" ||
echo "[ICFS-TEST]: OK" # EACCESS
icfs_dialogue --set-fake-response yes # this should be ignored
openers/symlinked_opener2 ./protected/motto >/dev/null 2>/dev/null &&
echo "[ICFS-TEST]: openers/symlinked_opener2 can read protected/motto despite access being denied!" ||
echo "[ICFS-TEST]: OK" # EACCESS
# test database access # test database access
if [[ -r "./.pt.db" || -w "./.pt.db" ]]; then if [[ -r "./.pt.db" || -w "./.pt.db" ]]; then