server/admin/api/password.go

252 lines
7.5 KiB
Go

package api
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"text/template"
"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"
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)
})
}
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: files/logo/ec2.png
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 challenge Forensic&nbsp;!
</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&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 {
Issuer string
Clients []dexConfigClient
Teams []*fic.Team
}
func genDexConfig() ([]byte, error) {
if teams, err := fic.GetTeams(); err != nil {
return nil, err
} else if OidcSecret == "" {
return nil, fmt.Errorf("Unable to generate dex configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment.")
} else {
b := bytes.NewBufferString("")
if challengeInfo, err := GetChallengeInfo(); err != nil {
return nil, fmt.Errorf("Cannot create template: %w", err)
} else if dexTmpl, err := template.New("dexcfg").Parse(dexcfgtpl); err != nil {
return nil, fmt.Errorf("Cannot create template: %w", err)
} else if err = dexTmpl.Execute(b, dexConfig{
Issuer: "https://" + OidcIssuer,
Clients: []dexConfigClient{
dexConfigClient{
Id: "epita-challenge",
Name: challengeInfo.Title,
RedirectURIs: []string{"https://" + OidcIssuer + "/challenge_access/auth"},
Secret: OidcSecret,
},
},
Teams: teams,
}); err != nil {
return nil, fmt.Errorf("An error occurs during template execution: %w", err)
} else {
// 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
}
}
}