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" . {{ "}}" }}

Bienvenue au {{ .Name }} !

{{ "{{" }} if .Invalid {{ "}}" }}
Identifiants incorrects.
{{ "{{" }} end {{ "}}" }}
{{ "{{" }} if .BackLink {{ "}}" }} {{ "{{" }} end {{ "}}" }}
{{ "{{" }} 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 } }