2021-09-09 09:20:45 +00:00
package api
import (
"bytes"
"fmt"
"io/ioutil"
2022-05-16 09:38:46 +00:00
"log"
"net/http"
2022-06-04 16:10:09 +00:00
"os"
2021-09-09 09:20:45 +00:00
"path"
2024-03-24 18:19:44 +00:00
"strings"
2021-09-09 09:20:45 +00:00
"text/template"
2024-03-24 18:19:44 +00:00
"unicode"
2021-09-09 09:20:45 +00:00
"srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic"
2022-05-16 09:38:46 +00:00
"github.com/gin-gonic/gin"
2021-09-09 09:20:45 +00:00
)
2024-03-23 17:00:42 +00:00
var (
2024-03-23 17:51:53 +00:00
OidcIssuer = "live.fic.srs.epita.fr"
OidcClientId = "epita-challenge"
OidcSecret = ""
2024-03-23 17:00:42 +00:00
)
2021-09-09 09:20:45 +00:00
2022-05-16 09:38:46 +00:00
func declarePasswordRoutes ( router * gin . RouterGroup ) {
router . POST ( "/password" , func ( c * gin . Context ) {
passwd , err := fic . GeneratePassword ( )
if err != nil {
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , gin . H { "password" : passwd } )
} )
2022-05-31 12:54:19 +00:00
router . GET ( "/dex.yaml" , func ( c * gin . Context ) {
2022-05-16 09:38:46 +00:00
cfg , err := genDexConfig ( )
if err != nil {
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : err . Error ( ) } )
return
}
c . String ( http . StatusOK , string ( cfg ) )
} )
2022-05-31 12:54:19 +00:00
router . POST ( "/dex.yaml" , func ( c * gin . Context ) {
2022-05-16 09:38:46 +00:00
if dexcfg , err := genDexConfig ( ) ; err != nil {
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : err . Error ( ) } )
return
} else if err := ioutil . WriteFile ( path . Join ( pki . PKIDir , "shared" , "dex-config.yaml" ) , [ ] byte ( dexcfg ) , 0644 ) ; err != nil {
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , true )
} )
2022-05-31 12:54:19 +00:00
router . GET ( "/dex-password.tpl" , func ( c * gin . Context ) {
2022-05-16 09:38:46 +00:00
passtpl , err := genDexPasswordTpl ( )
if err != nil {
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : err . Error ( ) } )
return
}
c . String ( http . StatusOK , string ( passtpl ) )
} )
2022-05-31 12:54:19 +00:00
router . POST ( "/dex-password.tpl" , func ( c * gin . Context ) {
2022-05-16 09:38:46 +00:00
if dexcfg , err := genDexPasswordTpl ( ) ; err != nil {
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : err . Error ( ) } )
return
} else if err := ioutil . WriteFile ( path . Join ( pki . PKIDir , "shared" , "dex-password.tpl" ) , [ ] byte ( dexcfg ) , 0644 ) ; err != nil {
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : err . Error ( ) } )
return
}
2024-03-23 17:51:53 +00:00
c . JSON ( http . StatusOK , true )
} )
router . GET ( "/vouch-proxy.yaml" , func ( c * gin . Context ) {
cfg , err := genVouchProxyConfig ( )
if err != nil {
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : err . Error ( ) } )
return
}
c . String ( http . StatusOK , string ( cfg ) )
} )
router . POST ( "/vouch-proxy.yaml" , func ( c * gin . Context ) {
if dexcfg , err := genVouchProxyConfig ( ) ; err != nil {
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : err . Error ( ) } )
return
} else if err := ioutil . WriteFile ( path . Join ( pki . PKIDir , "shared" , "vouch-config.yaml" ) , [ ] byte ( dexcfg ) , 0644 ) ; err != nil {
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : err . Error ( ) } )
return
}
2022-05-16 09:38:46 +00:00
c . JSON ( http . StatusOK , true )
} )
}
func declareTeamsPasswordRoutes ( router * gin . RouterGroup ) {
router . GET ( "/password" , func ( c * gin . Context ) {
team := c . MustGet ( "team" ) . ( * fic . Team )
2022-05-31 12:53:26 +00:00
if team . Password != nil {
c . String ( http . StatusOK , * team . Password )
} else {
c . AbortWithStatusJSON ( http . StatusNotFound , nil )
}
2022-05-16 09:38:46 +00:00
} )
router . POST ( "/password" , func ( c * gin . Context ) {
team := c . MustGet ( "team" ) . ( * fic . Team )
if passwd , err := fic . GeneratePassword ( ) ; err != nil {
log . Println ( "Unable to GeneratePassword:" , err . Error ( ) )
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : "Something went wrong when generating the new team password" } )
return
} else {
team . Password = & passwd
2022-05-31 12:53:53 +00:00
_ , err := team . Update ( )
2022-05-16 09:38:46 +00:00
if err != nil {
log . Println ( "Unable to Update Team:" , err . Error ( ) )
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : "Something went wrong when updating the new team password" } )
return
2021-09-09 09:20:45 +00:00
}
2022-05-16 09:38:46 +00:00
2022-05-31 12:53:53 +00:00
c . JSON ( http . StatusOK , team )
2022-05-16 09:38:46 +00:00
}
} )
2021-09-09 09:20:45 +00:00
}
2024-03-23 17:00:42 +00:00
const dexcfgtpl = ` issuer : { { . Issuer } }
2021-09-09 09:20:45 +00:00
storage :
type : sqlite3
config :
file : / var / dex / dex . db
web :
http : 0.0 .0 .0 : 5556
frontend :
issuer : Challenge forensic
2024-03-24 18:19:44 +00:00
logoURL : { { . LogoPath } }
2021-09-09 09:20:45 +00:00
dir : / srv / dex / web /
oauth2 :
skipApprovalScreen : true
staticClients :
{ { range $ c := . Clients } }
- id : { { $ c . Id } }
name : { { $ c . Name } }
redirectURIs : [ { { range $ u := $ c . RedirectURIs } } ' { { $ u } } ' { { end } } ]
secret : { { $ c . Secret } }
{ { end } }
enablePasswordDB : true
staticPasswords :
{ { range $ t := . Teams } }
- email : "team{{ printf " % 02 d " $t.Id }}"
hash : "{{with $t }}{{ .HashedPassword }}{{end}}"
{ { end } }
`
const dexpasswdtpl = ` { { "{{" } } template "header.html" . { { "}}" } }
< div class = "theme-panel" >
< h2 class = "theme-heading" >
2024-03-24 18:19:44 +00:00
Bienvenue au { { . Name } } & nbsp ; !
2021-09-09 09:20:45 +00:00
< / h2 >
< form method = "post" action = "{{ " { { " }} .PostURL {{ " } } " }}" >
< div class = "theme-form-row" >
< div class = "theme-form-label" >
< label for = "userid" > Votre équipe < / label >
< / div >
< select tabindex = "1" required id = "login" name = "login" class = "theme-form-input" autofocus >
{ { range $ t := . Teams } } < option value = "team{{ printf " % 02 d " $t.Id }}" > { { $ t . Name } } < / option >
{ { end } } < / select >
< / div >
< div class = "theme-form-row" >
< div class = "theme-form-label" >
< label for = "password" > Mot de passe < / label >
< / div >
< input tabindex = "2" required id = "password" name = "password" type = "password" class = "theme-form-input" placeholder = "mot de passe" { { "{{" } } if . Invalid { { "}}" } } autofocus { { "{{" } } end { { "}}" } } / >
< / div >
{ { "{{" } } if . Invalid { { "}}" } }
< div id = "login-error" class = "dex-error-box" >
Identifiants incorrects .
< / div >
{ { "{{" } } end { { "}}" } }
< button tabindex = "3" id = "submit-login" type = "submit" class = "dex-btn theme-btn--primary" > C ' est parti & nbsp ; ! < / button >
< / form >
{ { "{{" } } if . BackLink { { "}}" } }
< div class = "theme-link-back" >
< a class = "dex-subtle-text" href = "{{ " { { " }} .BackLink {{ " } } " }}" > Sélectionner une autre méthode d ' authentification . < / a >
< / div >
{ { "{{" } } end { { "}}" } }
< / div >
{ { "{{" } } template "footer.html" . { { "}}" } }
`
type dexConfigClient struct {
Id string
Name string
RedirectURIs [ ] string
Secret string
}
type dexConfig struct {
2024-03-24 18:19:44 +00:00
Name string
Issuer string
Clients [ ] dexConfigClient
Teams [ ] * fic . Team
LogoPath string
2021-09-09 09:20:45 +00:00
}
func genDexConfig ( ) ( [ ] byte , error ) {
2024-03-24 18:19:44 +00:00
if OidcSecret == "" {
2021-09-09 09:20:45 +00:00
return nil , fmt . Errorf ( "Unable to generate dex configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment." )
2024-03-24 18:19:44 +00:00
}
2021-09-09 09:20:45 +00:00
2024-03-24 18:19:44 +00:00
teams , err := fic . GetTeams ( )
if err != nil {
return nil , err
}
b := bytes . NewBufferString ( "" )
challengeInfo , err := GetChallengeInfo ( )
if err != nil {
return nil , fmt . Errorf ( "Cannot create template: %w" , err )
}
// Lower the first letter to be included in a sentence.
name := [ ] rune ( challengeInfo . Title )
if len ( name ) > 0 {
name [ 0 ] = unicode . ToLower ( name [ 0 ] )
}
logoPath := ""
if len ( challengeInfo . MainLogo ) > 0 {
logoPath = strings . Replace ( challengeInfo . MainLogo [ len ( challengeInfo . MainLogo ) - 1 ] , "$FILES$" , fic . FilesDir , - 1 )
}
dexTmpl , err := template . New ( "dexcfg" ) . Parse ( dexcfgtpl )
if err != nil {
return nil , fmt . Errorf ( "Cannot create template: %w" , err )
}
err = dexTmpl . Execute ( b , dexConfig {
Name : string ( name ) ,
Issuer : "https://" + OidcIssuer ,
Clients : [ ] dexConfigClient {
dexConfigClient {
Id : OidcClientId ,
Name : challengeInfo . Title ,
RedirectURIs : [ ] string { "https://" + OidcIssuer + "/challenge_access/auth" } ,
Secret : OidcSecret ,
2021-09-09 09:20:45 +00:00
} ,
2024-03-24 18:19:44 +00:00
} ,
Teams : teams ,
LogoPath : logoPath ,
} )
if err != nil {
return nil , fmt . Errorf ( "An error occurs during template execution: %w" , err )
}
2022-06-04 16:10:09 +00:00
2024-03-24 18:19:44 +00:00
// Also generate team associations
for _ , team := range teams {
if _ , err := os . Stat ( path . Join ( TeamsDir , fmt . Sprintf ( "team%02d" , team . Id ) ) ) ; err == nil {
if err = os . Remove ( path . Join ( TeamsDir , fmt . Sprintf ( "team%02d" , team . Id ) ) ) ; err != nil {
log . Println ( "Unable to remove existing association symlink:" , err . Error ( ) )
return nil , fmt . Errorf ( "Unable to remove existing association symlink: %s" , err . Error ( ) )
}
}
if err := os . Symlink ( fmt . Sprintf ( "%d" , team . Id ) , path . Join ( TeamsDir , fmt . Sprintf ( "team%02d" , team . Id ) ) ) ; err != nil {
log . Println ( "Unable to create association symlink:" , err . Error ( ) )
return nil , fmt . Errorf ( "Unable to create association symlink: %s" , err . Error ( ) )
2021-09-09 09:20:45 +00:00
}
}
2024-03-24 18:19:44 +00:00
return b . Bytes ( ) , nil
2021-09-09 09:20:45 +00:00
}
func genDexPasswordTpl ( ) ( [ ] byte , error ) {
if teams , err := fic . GetTeams ( ) ; err != nil {
return nil , err
} else {
b := bytes . NewBufferString ( "" )
if dexTmpl , err := template . New ( "dexpasswd" ) . Parse ( dexpasswdtpl ) ; err != nil {
return nil , fmt . Errorf ( "Cannot create template: %w" , err )
} else if err = dexTmpl . Execute ( b , dexConfig {
Teams : teams ,
} ) ; err != nil {
return nil , fmt . Errorf ( "An error occurs during template execution: %w" , err )
} else {
return b . Bytes ( ) , nil
}
}
}
2024-03-23 17:51:53 +00:00
const vouchcfgtpl = ` # CONFIGURATION FILE HANDLED BY fic - admin
# DO NOT MODIFY IT BY HAND
vouch :
logLevel : debug
allowAllUsers : true
document_root : / challenge_access
cookie :
2024-03-23 17:51:53 +00:00
domain : { { . Domain } }
oauth :
provider : oidc
client_id : { { . ClientId } }
client_secret : { { . ClientSecret } }
callback_urls :
- https : //{{ .Domain }}/challenge_access/auth
auth_url : https : //{{ .Domain }}/auth
token_url : http : //127.0.0.1:5556/token
user_info_url : http : //127.0.0.1:5556/userinfo
scopes :
- openid
- email
2024-03-23 17:51:53 +00:00
`
type vouchProxyConfig struct {
2024-03-23 17:51:53 +00:00
Domain string
2024-03-23 17:51:53 +00:00
ClientId string
ClientSecret string
}
func genVouchProxyConfig ( ) ( [ ] byte , error ) {
if OidcSecret == "" {
return nil , fmt . Errorf ( "Unable to generate vouch proxy configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment." )
}
b := bytes . NewBufferString ( "" )
if vouchTmpl , err := template . New ( "vouchcfg" ) . Parse ( vouchcfgtpl ) ; err != nil {
return nil , fmt . Errorf ( "Cannot create template: %w" , err )
} else if err = vouchTmpl . Execute ( b , vouchProxyConfig {
2024-03-23 17:51:53 +00:00
Domain : OidcIssuer ,
2024-03-23 17:51:53 +00:00
ClientId : OidcClientId ,
ClientSecret : OidcSecret ,
} ) ; err != nil {
return nil , fmt . Errorf ( "An error occurs during template execution: %w" , err )
} else {
return b . Bytes ( ) , nil
}
}