admin: Handle team password
This commit is contained in:
parent
ed69dc6ba4
commit
5eeb1a6297
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 !
|
||||
</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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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++; },
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue