351 lines
9.9 KiB
Go
351 lines
9.9 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"text/template"
|
|
"unicode"
|
|
|
|
"srs.epita.fr/fic-server/admin/pki"
|
|
"srs.epita.fr/fic-server/libfic"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
var (
|
|
OidcIssuer = "live.fic.srs.epita.fr"
|
|
OidcClientId = "epita-challenge"
|
|
OidcSecret = ""
|
|
)
|
|
|
|
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})
|
|
})
|
|
router.GET("/dex.yaml", func(c *gin.Context) {
|
|
cfg, err := genDexConfig()
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.String(http.StatusOK, string(cfg))
|
|
})
|
|
router.POST("/dex.yaml", func(c *gin.Context) {
|
|
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)
|
|
})
|
|
router.GET("/dex-password.tpl", func(c *gin.Context) {
|
|
passtpl, err := genDexPasswordTpl()
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.String(http.StatusOK, string(passtpl))
|
|
})
|
|
router.POST("/dex-password.tpl", func(c *gin.Context) {
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
c.JSON(http.StatusOK, true)
|
|
})
|
|
}
|
|
|
|
func declareTeamsPasswordRoutes(router *gin.RouterGroup) {
|
|
router.GET("/password", func(c *gin.Context) {
|
|
team := c.MustGet("team").(*fic.Team)
|
|
|
|
if team.Password != nil {
|
|
c.String(http.StatusOK, *team.Password)
|
|
} else {
|
|
c.AbortWithStatusJSON(http.StatusNotFound, nil)
|
|
}
|
|
})
|
|
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
|
|
|
|
_, err := team.Update()
|
|
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
|
|
}
|
|
|
|
c.JSON(http.StatusOK, team)
|
|
}
|
|
})
|
|
}
|
|
|
|
const dexcfgtpl = `issuer: {{ .Issuer }}
|
|
storage:
|
|
type: sqlite3
|
|
config:
|
|
file: /var/dex/dex.db
|
|
web:
|
|
http: 0.0.0.0:5556
|
|
frontend:
|
|
issuer: Challenge forensic
|
|
logoURL: {{ .LogoPath }}
|
|
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 "%02d" $t.Id }}"
|
|
hash: "{{with $t }}{{ .HashedPassword }}{{end}}"
|
|
{{ end }}
|
|
`
|
|
|
|
const dexpasswdtpl = `{{ "{{" }} template "header.html" . {{ "}}" }}
|
|
|
|
<div class="theme-panel">
|
|
<h2 class="theme-heading">
|
|
Bienvenue au {{ .Name }} !
|
|
</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 "%02d" $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 !</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 {
|
|
Name string
|
|
Issuer string
|
|
Clients []dexConfigClient
|
|
Teams []*fic.Team
|
|
LogoPath string
|
|
}
|
|
|
|
func genDexConfig() ([]byte, error) {
|
|
if OidcSecret == "" {
|
|
return nil, fmt.Errorf("Unable to generate dex configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment.")
|
|
}
|
|
|
|
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,
|
|
},
|
|
},
|
|
Teams: teams,
|
|
LogoPath: logoPath,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("An error occurs during template execution: %w", err)
|
|
}
|
|
|
|
// 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())
|
|
}
|
|
}
|
|
|
|
return b.Bytes(), nil
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
const vouchcfgtpl = `# CONFIGURATION FILE HANDLED BY fic-admin
|
|
# DO NOT MODIFY IT BY HAND
|
|
|
|
vouch:
|
|
logLevel: debug
|
|
allowAllUsers: true
|
|
document_root: /challenge_access
|
|
|
|
cookie:
|
|
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
|
|
`
|
|
|
|
type vouchProxyConfig struct {
|
|
Domain string
|
|
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{
|
|
Domain: OidcIssuer,
|
|
ClientId: OidcClientId,
|
|
ClientSecret: OidcSecret,
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("An error occurs during template execution: %w", err)
|
|
} else {
|
|
return b.Bytes(), nil
|
|
}
|
|
}
|