From 18b2647b1246dbed8a1c4f2ecde9ba470be2c130 Mon Sep 17 00:00:00 2001 From: Mysaa Java Date: Wed, 28 Jul 2021 22:52:43 +0200 Subject: [PATCH] Ajout de la Webterface, renommage du SQL --- .gitignore | 1 + Makefile | 10 +- bash-gitonly.c | 27 +--- cgit-config-gen.c | 6 +- clone-all.sh | 34 +++++ pam_oath_key.c | 9 ++ sql/AccessType.sql | 21 +-- ...{ReadableRepos.sql => AccessibleRepos.sql} | 13 +- webterface.c | 144 ++++++++++++++++++ webterface.cgi | Bin 0 -> 18644 bytes 10 files changed, 224 insertions(+), 41 deletions(-) create mode 100755 clone-all.sh rename sql/{ReadableRepos.sql => AccessibleRepos.sql} (71%) create mode 100644 webterface.c create mode 100755 webterface.cgi diff --git a/.gitignore b/.gitignore index 3b764c7..002744d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ auth-keys-gen cgit-config-gen +gen-readable-list bash-gitonly *.so *.o diff --git a/Makefile b/Makefile index 5bcc619..180ee14 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: all install -all: bash-gitonly pam_oath_key.so auth-keys-gen cgit-config-gen +all: bash-gitonly pam_oath_key.so auth-keys-gen cgit-config-gen webterface.cgi argvt.o: nargv/nargv.c gcc -c nargv/nargv.c -o argvt.o @@ -14,9 +14,11 @@ auth-keys-gen: authKeysPg.c gcc authKeysPg.c -o auth-keys-gen -L/usr/server/postgresql/lib -I/usr/server/postgresql/include -lpq cgit-config-gen: cgit-config-gen.c gcc cgit-config-gen.c -o cgit-config-gen -L/usr/server/postgresql/lib -I/usr/server/postgresql/include -lpq $(CFLAGS) +webterface.cgi: webterface.c + gcc webterface.c -o webterface.cgi -L/usr/server/postgresql/lib -I/usr/server/postgresql/include -lpq $(CFLAGS) -install-sql: sql/AccessType.sql sql/ReadableRepos.sql - /usr/server/postgresql/bin/psql -Upostgres -dpipi -f sql/AccessType.sql -f sql/ReadableRepos.sql +install-sql: sql/AccessType.sql sql/AccessibleRepos.sql + /usr/server/postgresql/bin/psql -Upostgres -dpipi -f sql/AccessType.sql -f sql/AccessibleRepos.sql install: all cp pam_oath_key.so /lib/security @@ -24,3 +26,5 @@ install: all cp bash-gitonly /srv/git/bin cp auth-keys-gen /srv/etc/auth-git-keys cp cgit-config-gen /srv/etc/cgit-config-gen + cp webterface.cgi /srv/admin/webterface/webterface.cgi + cp clone-all.sh /srv/admin/public/clone-all.sh diff --git a/bash-gitonly.c b/bash-gitonly.c index 3088b97..86f74b9 100644 --- a/bash-gitonly.c +++ b/bash-gitonly.c @@ -59,7 +59,7 @@ int main(int argc, char **argv, char **envp){ PGresult *res; int nFields; - const char *accessTypeRequestValues[2]; + const char *accessTypeRequestValues[3]; char* niveauAutorisation; char execFilename[MAX_FULL_EXECFILENAME_LENGTH] = COMMANDS_PATH; @@ -142,8 +142,8 @@ int main(int argc, char **argv, char **envp){ // Récupération de l'autorisation auprès du serveur Pgsql accessTypeRequestValues[0] = userID; accessTypeRequestValues[1] = repoName; - - res = PQexecParams(conn, "SELECT git.\"AccessType\"($1,$2)",2,NULL,accessTypeRequestValues,NULL,NULL,1); + accessTypeRequestValues[2] = (strcmp(gitargv->argv[0],"git-receive-pack")==0)?"WRITE":"READ"; + res = PQexecParams(conn, "SELECT * FROM git.\"AccessibleRepos\"($1,$3) WHERE path=$2;",3,NULL,accessTypeRequestValues,NULL,NULL,1); if (PQresultStatus(res) != PGRES_TUPLES_OK) { ERR("Impossible de lancer la requête SQL pour les autorisations: %s",PQerrorMessage(conn)); PQclear(res); @@ -153,28 +153,15 @@ int main(int argc, char **argv, char **envp){ } nFields = PQntuples(res); - // Il y a toujours une seule valeur retournée, renvoie NULL si il n'y a rien - niveauAutorisation = PQgetvalue(res, 0, 0); PQclear(res); PQfinish(conn); - //ERR("Autorisation: sur %s pour l'id %s -> %s",repoName,userID,niveauAutorisation)); - if(niveauAutorisation[0]=='\0'){// If the string is empty i.e. NULL authorisations + + if(nFields<1){ ERR("Vous n'avez pas le droit d'accéder à ce repo. Il n'existe peut-être même pas ..."); nargv_free(gitargv); return 1; } - - // Déjà on a la lecture - - if(strcmp(gitargv->argv[0],"git-receive-pack")==0){ - // Il nous faut aussi le droit d'écriture - if(strcmp(niveauAutorisation,"WRITE")!=0){ - ERR("Vous n'avez pas le droit d'écrire dans ce repo."); - nargv_free(gitargv); - return 1; - } - } - + // On effectue la commande. // Pour l'instant execFilename = COMMANDS_PATH; strcat(execFilename, gitargv->argv[0]); @@ -192,5 +179,5 @@ int main(int argc, char **argv, char **envp){ nargv_free(gitargv); - return 1; + return 0; } diff --git a/cgit-config-gen.c b/cgit-config-gen.c index b4e06ce..28c5d45 100644 --- a/cgit-config-gen.c +++ b/cgit-config-gen.c @@ -92,10 +92,10 @@ main(int argc, char **argv) } PQclear(res); - + // Demande les données à la BDD const char const * sqlParamValues[] = {"0"}; - res = PQexecParams(conn,"SELECT path,owner,description,logoUrl,groupeID,gdr.nom FROM git.\"ReadableRepos\"($1) JOIN git.\"groupesDeRepo\" AS gdr ON gdr.\"ID\"=groupeID ORDER BY groupeID",1,NULL,sqlParamValues,NULL,NULL,0); + res = PQexecParams(conn,"SELECT path,owner,description,logoUrl,groupeID,gdr.nom FROM git.\"AccessibleRepos\"($1,'READ') JOIN git.\"groupesDeRepo\" AS gdr ON gdr.\"ID\"=groupeID ORDER BY groupeID",1,NULL,sqlParamValues,NULL,NULL,0); if (PQresultStatus(res) != PGRES_TUPLES_OK) { fprintf(stderr,"Impossible de lancer la requête SQL pour la liste des repos: %s",PQerrorMessage(conn)); PQclear(res); @@ -106,7 +106,7 @@ main(int argc, char **argv) // Renvoie le résultat à travers stdout for (i = 0; i < PQntuples(res); i++) { - + if(PQgetisnull(res,i,4)!=1 && (strcmp(PQgetvalue(res, i, 4),lastRgid)!=0)){ lastRgid = PQgetvalue(res, i, 4); printf("\nsection=%s\n\n",PQgetvalue(res,i,5)); diff --git a/clone-all.sh b/clone-all.sh new file mode 100755 index 0000000..4c506ce --- /dev/null +++ b/clone-all.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +token=FgBxePzpabd+9D1EJWrZ4tEbY6UQ5QLFR+7tdrtfCqFbN210trnyAynByxlsb2voDkz1oR1yoZkPR9mMMwPxjw +liste=`curl https://admin.bernard.com.de/wtf/$token/list-readable-repos` + +for path in $liste +do + + if [ -f ./$path ] + then + echo "Vérification du repo $path" + if [[ ! -z $(git -C "./$path" status -s) ]] + then + echo "Le repository $path a des fichiers non enregistrés dans un commit... faudrait y corriger" + git -C "./$path" status + exit 1 + fi + # Bon. on télécharge + if git -C "./$path" pull + then + /bin/true # Ou bien rien à merge, ou bien ca à marché. + else + # Impossible de merge + echo "Le repository $path a suivi un chemin de commits différent, il faut résoudre ça à la main." + exit 1 + fi + # Enfin, on envoie nos changements + git -C "./$path" push --all + + else + git clone git@bernard.com.de:$path ./$path + fi + +done diff --git a/pam_oath_key.c b/pam_oath_key.c index d500d1d..115007c 100644 --- a/pam_oath_key.c +++ b/pam_oath_key.c @@ -166,6 +166,15 @@ pam_sm_authenticate(pam_handle_t * pamh, int flags, int argc, const char ** argv D("Running the oath authenticator !\n"); + + D("Environement récupéré:\n"); + + char** envp = pam_getenvlist(pamh); + for (char **env = envp; *env != 0; env++) + { + char *thisEnv = *env; + printf("%s\n", thisEnv); + } /***** Parsing config *****/ parse_cfg(pamh, flags, argc, argv, &cfg); diff --git a/sql/AccessType.sql b/sql/AccessType.sql index f6f86c0..4fc3ae6 100644 --- a/sql/AccessType.sql +++ b/sql/AccessType.sql @@ -8,7 +8,10 @@ AS $code$ DECLARE selectedrepo git.gitrepoaccesstype; BEGIN - + + IF gitrepo LIKE '/%' + THEN gitrepo := SUBSTRING(gitrepo,1,LENGTH(gitrepo)-1); + END IF; /* user.repo */ SELECT MAX("accessType") INTO selectedrepo @@ -16,26 +19,26 @@ BEGIN JOIN git.repos ON "repoID"=repos."ID" WHERE repos.path=gitrepo AND "userID"=gituserid; - RETURN selectedrepo; - IF selectedrepo != NULL THEN RETURN selectedrepo; END IF; - + RAISE WARNING 'Le repo na pas marche 2'; /* groupe.repo */ - SELECT MAX('accessType') + SELECT MAX(perms."accessType") INTO selectedrepo FROM git.perms - JOIN git.repos ON repoID=repos."ID" + JOIN git.repos ON "repoID"=repos."ID" JOIN git.appartenances ON appartenances."groupeID"=-perms."userID" WHERE repos.path=gitrepo AND appartenances."utilisateurID"=gituserid; + IF selectedrepo != NULL THEN RETURN selectedrepo; END IF; + RAISE WARNING 'Le repo na pas marche 3'; /* user.repoGroup */ - SELECT MAX('accessType') + SELECT MAX("accessType") INTO selectedrepo FROM git.perms JOIN git.repos ON "repoID"=-repos."groupeID" @@ -44,9 +47,9 @@ BEGIN IF selectedrepo != NULL THEN RETURN selectedrepo; END IF; - + RAISE WARNING 'Le repo na pas marche 4'; /* groupe.repoGroupe */ - SELECT MAX('accessType') + SELECT MAX("accessType") INTO selectedrepo FROM git.perms JOIN git.appartenances ON appartenances."groupeID"=-perms."userID" diff --git a/sql/ReadableRepos.sql b/sql/AccessibleRepos.sql similarity index 71% rename from sql/ReadableRepos.sql rename to sql/AccessibleRepos.sql index f76ab23..75e01ac 100644 --- a/sql/ReadableRepos.sql +++ b/sql/AccessibleRepos.sql @@ -1,5 +1,6 @@ -CREATE OR REPLACE FUNCTION git."ReadableRepos" ( - gituserid integer +CREATE OR REPLACE FUNCTION git."AccessibleRepos" ( + gituserid integer, + accesst git.gitrepoaccesstype ) RETURNS table( ID integer, @@ -18,26 +19,26 @@ BEGIN SELECT repos.* FROM git.perms JOIN git.repos ON "repoID"=repos."ID" - WHERE "accessType">='READ' AND "userID"=gituserid + WHERE "accessType">=accesst AND "userID"=gituserid UNION /* groupe.repo */ SELECT repos.* FROM git.perms JOIN git.repos ON "repoID"=repos."ID" JOIN git.appartenances ON appartenances."groupeID"=-perms."userID" - WHERE "accessType">='READ' AND appartenances."utilisateurID"=gituserid + WHERE "accessType">=accesst AND appartenances."utilisateurID"=gituserid UNION /* user.repoGroup */ SELECT repos.* FROM git.perms JOIN git.repos ON "repoID"=-repos."groupeID" - WHERE "accessType">='READ' AND "userID"=gituserid + WHERE "accessType">=accesst AND "userID"=gituserid UNION /* groupe.repoGroupe */ SELECT repos.* FROM git.perms JOIN git.appartenances ON appartenances."groupeID"=-perms."userID" JOIN git.repos ON "repoID"=-repos."groupeID" - WHERE "accessType">='READ' AND "userID"=gituserid; + WHERE "accessType">=accesst AND "userID"=gituserid; END $code$ diff --git a/webterface.c b/webterface.c new file mode 100644 index 0000000..2ebf001 --- /dev/null +++ b/webterface.c @@ -0,0 +1,144 @@ +#include +#include +#include "libpq-fe.h" +#include + +#include +#include +#include +#include + +#include + +#define BDD_PASS_FILE "/srv/bdd/pipi-system.pass" +#define BDD_CONN_LENGTH 255 + +int +main(int argc, char **argv, char **envp) +{ + + // Print cgi headers + printf("Content-Type: text/plain; charset=UTF-8\n\n"); + + char* pathInfo = getenv("PATH_INFO"); + if(pathInfo==NULL){ + printf("Je n'ai pas accès au PATH_INFO.\n"); + return 2; + } + + const char slash[] = {'/'}; + char* token = strtok(pathInfo,slash); + if(token == NULL){ + printf("Il vous faut un token non nul pour accéder à l'interface\n"); + return 2; + } + char* action = strtok(NULL,slash); + if(action == NULL){ + printf("Que voulez-vous faire avec votre token %s ?\n",token); + return 2; + } + char* reste = strtok(NULL,slash); + + //printf("Données récupérées: %s, puis %s, puis %s",token,action,reste); + int i; + /* + //Dumping args + for(i = 0; i < argc; i++) + printf("argv[%d] -> %s\n",i,argv[i]); + //Dumping env + for (char **env = envp; *env != 0; env++) + { + char *thisEnv = *env; + printf("env: %s\n", thisEnv); + } + + printf("-------------------------------------------\n"); + */ + char connInfo[BDD_CONN_LENGTH] = "host='/var/run/postgresql' dbname='pipi' user=pipisys password='"; + FILE *dbPassFile; + char ch; + int pos = strlen(connInfo); + + PGconn *conn; + PGresult *res; + + char* userId = "0"; + if(argc>1){ + userId=*(argv+1); + } + + // Récupère le mdp à la BDD + dbPassFile = fopen(BDD_PASS_FILE,"r"); + if (dbPassFile == NULL) { + fprintf(stderr,"Cannot open file %s, on peut pas se connecter à la base de données pour lister les clés en tant que %d -> fopen error %d\n", BDD_PASS_FILE,geteuid(),errno); + return 1; + } + while (feof(dbPassFile)) + { + connInfo[pos] = fgetc(dbPassFile); + pos++; + } + fclose(dbPassFile); + connInfo[pos] = '\''; + + + // Connecte à la BDD + conn = PQconnectdb(connInfo); + if (PQstatus(conn) != CONNECTION_OK) + { + fprintf(stderr, "Connection to database failed: %s", PQerrorMessage(conn)); + PQfinish(conn); + return 1; + } + res = PQexec(conn, + "SELECT pg_catalog.set_config('search_path', '', false)"); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + { + fprintf(stderr, "SET failed: %s", PQerrorMessage(conn)); + PQclear(res); + PQfinish(conn); + return 1; + } + PQclear(res); + + // Demande de l'autorisation + + char const * sqlParamValues[] = {action,token}; + res = PQexecParams(conn,"SELECT userID FROM git.tokens WHERE type=$1 AND token=$2",2,NULL,sqlParamValues,NULL,NULL,0); + if (PQresultStatus(res) != PGRES_TUPLES_OK) { + fprintf(stderr,"Impossible de lancer la requête SQL pour authentifier le token: %s",PQerrorMessage(conn)); + PQclear(res); + PQfinish(conn); + return 1; + } + if(PQntuples(res)<1){ + printf("Ce token est invalide !!!"); + PQclear(res); + PQfinish(conn); + return 0; + } + userId=PQgetvalue(res,0,0); + PQclear(res); + + // Demande les données à la BDD + sqlParamValues[0] = userId; + res = PQexecParams(conn,"SELECT path,owner,description,logoUrl,groupeID,gdr.nom FROM git.\"AccessibleRepos\"($1,'WRITE') JOIN git.\"groupesDeRepo\" AS gdr ON gdr.\"ID\"=groupeID ORDER BY groupeID",1,NULL,sqlParamValues,NULL,NULL,0); + if (PQresultStatus(res) != PGRES_TUPLES_OK) { + fprintf(stderr,"Impossible de lancer la requête SQL pour la liste des repos: %s",PQerrorMessage(conn)); + PQclear(res); + PQfinish(conn); + return 1; + } + + // Renvoie le résultat à travers stdout + for (i = 0; i < PQntuples(res); i++) + { + printf("%s\n",PQgetvalue(res,i,0)); + } + + + PQclear(res); + PQfinish(conn); + return 0; +} + diff --git a/webterface.cgi b/webterface.cgi new file mode 100755 index 0000000000000000000000000000000000000000..17038ceb5eee5820b212ec598ed9a8fb3ca40bcc GIT binary patch literal 18644 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 ZU