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()
// Let's pick a random password
password, err := pki.GeneratePassword()
password, err := fic.GeneratePassword()
if err != nil {
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
if *ut.Password == "" {
ut.Password = nil
}
if _, err := ut.Update(); err != nil {
return nil, err
}

View File

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

View File

@ -5,38 +5,12 @@ import (
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"os"
"strings"
)
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) {
if priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader); err == nil {
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() {
$http.post("api/disableinactiveteams").then(function() {
$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)
$routeParams.teamId = $scope.team.id;
$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() {
backName = this.team.name;
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>
</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">
<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>

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('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="genDexCfg()" class="float-right btn btn-sm btn-success mr-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> DexIdP</button>
</h2>
<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,
color INTEGER NOT NULL,
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;
`); err != nil {
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"
"math"
"time"
"golang.org/x/crypto/bcrypt"
)
// 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.
type Team struct {
Id int64 `json:"id"`
Name string `json:"name"`
Color uint32 `json:"color"`
Active bool `json:"active"`
ExternalId string `json:"external_id"`
Id int64 `json:"id"`
Name string `json:"name"`
Color uint32 `json:"color"`
Active bool `json:"active"`
ExternalId string `json:"external_id"`
Password *string `json:"password"`
}
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
} else {
defer rows.Close()
@ -30,7 +33,7 @@ func getTeams(filter string) ([]Team, error) {
var teams = make([]Team, 0)
for rows.Next() {
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
}
teams = append(teams, t)
@ -56,7 +59,7 @@ func GetActiveTeams() ([]Team, error) {
// GetTeam retrieves a Team from its identifier.
func GetTeam(id int64) (Team, error) {
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
}
@ -66,7 +69,7 @@ func GetTeam(id int64) (Team, error) {
// GetTeamBySerial retrieves a Team from one of its associated certificates.
func GetTeamBySerial(serial int64) (Team, error) {
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
}
@ -80,13 +83,13 @@ func CreateTeam(name string, color uint32, externalId string) (Team, error) {
} else if tid, err := res.LastInsertId(); err != nil {
return Team{}, err
} else {
return Team{tid, name, color, true, ""}, nil
return Team{tid, name, color, true, "", nil}, nil
}
}
// Update applies modifications back to the database.
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
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
@ -310,3 +313,18 @@ func (t Team) HasPartiallySolved(f Flag) (tm *time.Time) {
}
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
}