admin: Handle team password

This commit is contained in:
nemunaire 2021-09-09 11:20:45 +02:00
parent ed69dc6ba4
commit 5eeb1a6297
11 changed files with 299 additions and 40 deletions

View File

@ -254,7 +254,7 @@ func generateClientCert(_ httprouter.Params, _ []byte) (interface{}, error) {
serial := serial_b.Uint64() serial := serial_b.Uint64()
// Let's pick a random password // Let's pick a random password
password, err := pki.GeneratePassword() password, err := fic.GeneratePassword()
if err != nil { if err != nil {
return nil, err return nil, err
} }

195
admin/api/password.go Normal file
View File

@ -0,0 +1,195 @@
package api
import (
"bytes"
"fmt"
"io/ioutil"
"path"
"text/template"
"srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
)
var OidcSecret = ""
func init() {
router.POST("/api/password", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
if passwd, err := fic.GeneratePassword(); err != nil {
return nil, err
} else {
return map[string]string{"password": passwd}, nil
}
}))
router.GET("/api/teams/:tid/password", apiHandler(teamHandler(
func(team fic.Team, _ []byte) (interface{}, error) {
return team.Password, nil
})))
router.POST("/api/teams/:tid/password", apiHandler(teamHandler(
func(team fic.Team, _ []byte) (interface{}, error) {
if passwd, err := fic.GeneratePassword(); err != nil {
return nil, err
} else {
team.Password = &passwd
return team.Update()
}
})))
router.GET("/api/dex.yaml", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return genDexConfig()
}))
router.POST("/api/dex.yaml", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
if dexcfg, err := genDexConfig(); err != nil {
return nil, err
} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-config.yaml"), []byte(dexcfg), 0644); err != nil {
return nil, err
} else {
return true, nil
}
}))
router.GET("/api/dex-password.tpl", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
return genDexPasswordTpl()
}))
router.POST("/api/dex-password.tpl", apiHandler(
func(httprouter.Params, []byte) (interface{}, error) {
if dexcfg, err := genDexPasswordTpl(); err != nil {
return nil, err
} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-password.tpl"), []byte(dexcfg), 0644); err != nil {
return nil, err
} else {
return true, nil
}
}))
}
const dexcfgtpl = `issuer: https://fic.srs.epita.fr
storage:
type: sqlite3
config:
file: /var/dex/dex.db
web:
http: 0.0.0.0:5556
frontend:
issuer: Challenge forensic
logoURL: img/fic.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 {
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 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{
Clients: []dexConfigClient{
dexConfigClient{
Id: "epita-challenge",
Name: "Challenge Forensic",
RedirectURIs: []string{"https://fic.srs.epita.fr/challenge_access/auth"},
Secret: OidcSecret,
},
},
Teams: teams,
}); err != nil {
return nil, fmt.Errorf("An error occurs during template execution: %w", err)
} else {
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
}
}
}

View File

@ -171,6 +171,10 @@ func updateTeam(team fic.Team, body []byte) (interface{}, error) {
ut.Id = team.Id ut.Id = team.Id
if *ut.Password == "" {
ut.Password = nil
}
if _, err := ut.Update(); err != nil { if _, err := ut.Update(); err != nil {
return nil, err return nil, err
} }

View File

@ -71,6 +71,9 @@ func main() {
baseURL := "/" baseURL := "/"
// Read paremeters from environment // Read paremeters from environment
if v, exists := os.LookupEnv("FICOIDC_SECRET"); exists {
api.OidcSecret = v
}
if v, exists := os.LookupEnv("FICCA_PASS"); exists { if v, exists := os.LookupEnv("FICCA_PASS"); exists {
pki.SetCAPassword(v) pki.SetCAPassword(v)
} else { } else {

View File

@ -5,38 +5,12 @@ import (
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/x509" "crypto/x509"
"encoding/base64"
"encoding/pem" "encoding/pem"
"os" "os"
"strings"
) )
var PKIDir string var PKIDir string
func GeneratePassword() (password string, err error) {
// This will make a 12 chars long password
b := make([]byte, 9)
if _, err = rand.Read(b); err != nil {
return
}
password = base64.StdEncoding.EncodeToString(b)
// Avoid hard to read characters
for _, i := range [][2]string{
{"v", "*"}, {"u", "("},
{"l", "%"}, {"1", "?"},
{"o", "@"}, {"O", "!"}, {"0", ">"},
// This one is to avoid problem with openssl
{"/", "^"},
} {
password = strings.Replace(password, i[0], i[1], -1)
}
return
}
func GeneratePrivKey() (pub *ecdsa.PublicKey, priv *ecdsa.PrivateKey, err error) { func GeneratePrivKey() (pub *ecdsa.PublicKey, priv *ecdsa.PrivateKey, err error) {
if priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader); err == nil { if priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader); err == nil {
pub = &priv.PublicKey pub = &priv.PublicKey

View File

@ -1961,6 +1961,17 @@ angular.module("FICApp")
} }
}; };
$scope.genDexCfg = function() {
$http.post("api/dex.yaml").then(function() {
$http.post("api/dex-password.tpl").then(function() {
$scope.addToast('success', 'Dex config refreshed.', "Don't forget to reload/reboot frontend host.");
}, function(response) {
$scope.addToast('danger', 'An error occurs when generating dex password tpl:', response.data.errmsg);
});
}, function(response) {
$scope.addToast('danger', 'An error occurs when generating dex config:', response.data.errmsg);
});
}
$scope.desactiveTeams = function() { $scope.desactiveTeams = function() {
$http.post("api/disableinactiveteams").then(function() { $http.post("api/disableinactiveteams").then(function() {
$scope.teams = Team.query(); $scope.teams = Team.query();
@ -1993,7 +2004,7 @@ angular.module("FICApp")
} }
} }
}) })
.controller("TeamController", function($scope, $rootScope, $location, Team, TeamMember, $routeParams) { .controller("TeamController", function($scope, $rootScope, $location, Team, TeamMember, $routeParams, $http) {
if ($scope.team && $scope.team.id) if ($scope.team && $scope.team.id)
$routeParams.teamId = $scope.team.id; $routeParams.teamId = $scope.team.id;
$scope.team = Team.get({ teamId: $routeParams.teamId }); $scope.team = Team.get({ teamId: $routeParams.teamId });
@ -2010,6 +2021,14 @@ angular.module("FICApp")
}); });
} }
} }
$scope.resetPasswd = function(team) {
$http({
url: "api/password",
method: "POST"
}).then(function(response) {
team.password = response.data.password;
});
}
$scope.deleteTeam = function() { $scope.deleteTeam = function() {
backName = this.team.name; backName = this.team.name;
this.team.$remove(function() { $scope.addToast('success', 'Team ' + backName + ' successfully removed.'); $location.url("/teams/"); $rootScope.staticFilesNeedUpdate++; }, this.team.$remove(function() { $scope.addToast('success', 'Team ' + backName + ' successfully removed.'); $location.url("/teams/"); $rootScope.staticFilesNeedUpdate++; },

View File

@ -34,6 +34,19 @@
<input type="color" class="form-control form-control-sm" id="{{ field }}{{ member.id }}" ng-model="team[field]" ng-if="field == 'color'" color> <input type="color" class="form-control form-control-sm" id="{{ field }}{{ member.id }}" ng-model="team[field]" ng-if="field == 'color'" color>
</div> </div>
</div> </div>
<div class="row" ng-if="team.id">
<label for="passwd" class="col-sm-2 col-form-label-sm">Mot de passe</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control form-control-sm" id="passwd" ng-model="team.password">
<div class="input-group-append">
<button class="btn btn-sm btn-outline-danger" type="button" ng-click="resetPasswd(team)">
<span class="glyphicon glyphicon-random" aria-hidden="true"></span>
</button>
</div>
</div>
</div>
</div>
<div class="text-right" ng-show="team.id"> <div class="text-right" ng-show="team.id">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-save" aria-hidden="true"></span> Save</button> <button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-save" aria-hidden="true"></span> Save</button>
<button type="button" class="btn btn-danger" ng-click="deleteTeam()"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete</button> <button type="button" class="btn btn-danger" ng-click="deleteTeam()"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete</button>

View File

@ -4,6 +4,7 @@
<button type="button" ng-click="show('print')" class="float-right btn btn-sm btn-secondary mr-2"><span class="glyphicon glyphicon-print" aria-hidden="true"></span> Imprimer les équipes</button> <button type="button" ng-click="show('print')" class="float-right btn btn-sm btn-secondary mr-2"><span class="glyphicon glyphicon-print" aria-hidden="true"></span> Imprimer les équipes</button>
<button type="button" ng-click="show('export')" class="float-right btn btn-sm btn-secondary mr-2"><span class="glyphicon glyphicon-export" aria-hidden="true"></span> Statistiques générales</button> <button type="button" ng-click="show('export')" class="float-right btn btn-sm btn-secondary mr-2"><span class="glyphicon glyphicon-export" aria-hidden="true"></span> Statistiques générales</button>
<button type="button" ng-click="desactiveTeams()" class="float-right btn btn-sm btn-danger mr-2" title="Cliquer pour marquer les équipes sans certificat comme inactives (et ainsi éviter que ses fichiers ne soient générés)"><span class="glyphicon glyphicon-leaf" aria-hidden="true"></span> Désactiver les équipes inactives</button> <button type="button" ng-click="desactiveTeams()" class="float-right btn btn-sm btn-danger mr-2" title="Cliquer pour marquer les équipes sans certificat comme inactives (et ainsi éviter que ses fichiers ne soient générés)"><span class="glyphicon glyphicon-leaf" aria-hidden="true"></span> Désactiver les équipes inactives</button>
<button type="button" ng-click="genDexCfg()" class="float-right btn btn-sm btn-success mr-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> DexIdP</button>
</h2> </h2>
<p><input type="search" class="form-control" placeholder="Search" ng-model="query" ng-keypress="validateSearch($event)" autofocus></p> <p><input type="search" class="form-control" placeholder="Search" ng-model="query" ng-keypress="validateSearch($event)" autofocus></p>

View File

@ -99,7 +99,8 @@ CREATE TABLE IF NOT EXISTS teams(
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
color INTEGER NOT NULL, color INTEGER NOT NULL,
active BOOLEAN NOT NULL DEFAULT 1, active BOOLEAN NOT NULL DEFAULT 1,
external_id VARCHAR(255) NOT NULL external_id VARCHAR(255) NOT NULL,
password VARCHAR(255) NULL
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; ) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
`); err != nil { `); err != nil {
return err return err

31
libfic/password.go Normal file
View File

@ -0,0 +1,31 @@
package fic
import (
"crypto/rand"
"encoding/base64"
"strings"
)
func GeneratePassword() (password string, err error) {
// This will make a 12 chars long password
b := make([]byte, 9)
if _, err = rand.Read(b); err != nil {
return
}
password = base64.StdEncoding.EncodeToString(b)
// Avoid hard to read characters
for _, i := range [][2]string{
{"v", "*"}, {"u", "("},
{"l", "%"}, {"1", "?"},
{"o", "@"}, {"O", "!"}, {"0", ">"},
// This one is to avoid problem with openssl
{"/", "^"},
} {
password = strings.Replace(password, i[0], i[1], -1)
}
return
}

View File

@ -4,6 +4,8 @@ import (
"log" "log"
"math" "math"
"time" "time"
"golang.org/x/crypto/bcrypt"
) )
// UnlockedChallengeDepth is the number of challenges to unlock ahead (0: only the next one, -1: all) // UnlockedChallengeDepth is the number of challenges to unlock ahead (0: only the next one, -1: all)
@ -14,15 +16,16 @@ var WChoiceCoefficient = 1.0
// Team represents a group of players, come to solve our challenges. // Team represents a group of players, come to solve our challenges.
type Team struct { type Team struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Color uint32 `json:"color"` Color uint32 `json:"color"`
Active bool `json:"active"` Active bool `json:"active"`
ExternalId string `json:"external_id"` ExternalId string `json:"external_id"`
Password *string `json:"password"`
} }
func getTeams(filter string) ([]Team, error) { func getTeams(filter string) ([]Team, error) {
if rows, err := DBQuery("SELECT id_team, name, color, active, external_id FROM teams " + filter); err != nil { if rows, err := DBQuery("SELECT id_team, name, color, active, external_id, password FROM teams " + filter); err != nil {
return nil, err return nil, err
} else { } else {
defer rows.Close() defer rows.Close()
@ -30,7 +33,7 @@ func getTeams(filter string) ([]Team, error) {
var teams = make([]Team, 0) var teams = make([]Team, 0)
for rows.Next() { for rows.Next() {
var t Team var t Team
if err := rows.Scan(&t.Id, &t.Name, &t.Color, &t.Active, &t.ExternalId); err != nil { if err := rows.Scan(&t.Id, &t.Name, &t.Color, &t.Active, &t.ExternalId, &t.Password); err != nil {
return nil, err return nil, err
} }
teams = append(teams, t) teams = append(teams, t)
@ -56,7 +59,7 @@ func GetActiveTeams() ([]Team, error) {
// GetTeam retrieves a Team from its identifier. // GetTeam retrieves a Team from its identifier.
func GetTeam(id int64) (Team, error) { func GetTeam(id int64) (Team, error) {
var t Team var t Team
if err := DBQueryRow("SELECT id_team, name, color, active, external_id FROM teams WHERE id_team = ?", id).Scan(&t.Id, &t.Name, &t.Color, &t.Active, &t.ExternalId); err != nil { if err := DBQueryRow("SELECT id_team, name, color, active, external_id, password FROM teams WHERE id_team = ?", id).Scan(&t.Id, &t.Name, &t.Color, &t.Active, &t.ExternalId, &t.Password); err != nil {
return t, err return t, err
} }
@ -66,7 +69,7 @@ func GetTeam(id int64) (Team, error) {
// GetTeamBySerial retrieves a Team from one of its associated certificates. // GetTeamBySerial retrieves a Team from one of its associated certificates.
func GetTeamBySerial(serial int64) (Team, error) { func GetTeamBySerial(serial int64) (Team, error) {
var t Team var t Team
if err := DBQueryRow("SELECT T.id_team, T.name, T.color, T.active, T.external_id FROM certificates C INNER JOIN teams T ON T.id_team = C.id_team WHERE id_cert = ?", serial).Scan(&t.Id, &t.Name, &t.Color, &t.Active, &t.ExternalId); err != nil { if err := DBQueryRow("SELECT T.id_team, T.name, T.color, T.active, T.external_id, T.password FROM certificates C INNER JOIN teams T ON T.id_team = C.id_team WHERE id_cert = ?", serial).Scan(&t.Id, &t.Name, &t.Color, &t.Active, &t.ExternalId, &t.Password); err != nil {
return t, err return t, err
} }
@ -80,13 +83,13 @@ func CreateTeam(name string, color uint32, externalId string) (Team, error) {
} else if tid, err := res.LastInsertId(); err != nil { } else if tid, err := res.LastInsertId(); err != nil {
return Team{}, err return Team{}, err
} else { } else {
return Team{tid, name, color, true, ""}, nil return Team{tid, name, color, true, "", nil}, nil
} }
} }
// Update applies modifications back to the database. // Update applies modifications back to the database.
func (t Team) Update() (int64, error) { func (t Team) Update() (int64, error) {
if res, err := DBExec("UPDATE teams SET name = ?, color = ?, active = ?, external_id = ? WHERE id_team = ?", t.Name, t.Color, t.Active, t.ExternalId, t.Id); err != nil { if res, err := DBExec("UPDATE teams SET name = ?, color = ?, active = ?, external_id = ?, password = ? WHERE id_team = ?", t.Name, t.Color, t.Active, t.ExternalId, t.Password, t.Id); err != nil {
return 0, err return 0, err
} else if nb, err := res.RowsAffected(); err != nil { } else if nb, err := res.RowsAffected(); err != nil {
return 0, err return 0, err
@ -310,3 +313,18 @@ func (t Team) HasPartiallySolved(f Flag) (tm *time.Time) {
} }
return return
} }
// HashedPassword compute a bcrypt version of the team's password.
func (t Team) HashedPassword() (string, error) {
if t.Password == nil {
if passwd, err := GeneratePassword(); err != nil {
return "", err
} else {
h, err := bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost)
return string(h), err
}
}
h, err := bcrypt.GenerateFromPassword([]byte(*t.Password), bcrypt.DefaultCost)
return string(h), err
}