Introducing new PKI management
This commit is contained in:
parent
69640506d0
commit
0259ae8f94
@ -1,7 +1,167 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"srs.epita.fr/fic-server/admin/pki"
|
||||
"srs.epita.fr/fic-server/libfic"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func init() {
|
||||
router.GET("/api/ca/", apiHandler(infoCA))
|
||||
router.GET("/api/ca.pem", apiHandler(getCAPEM))
|
||||
router.POST("/api/ca/new", apiHandler(
|
||||
func(_ httprouter.Params, _ []byte) (interface{}, error) { return true, pki.GenerateCA(time.Date(2018, 01, 21, 0, 0, 0, 0, time.UTC), time.Date(2018, 01, 24, 23, 59, 59, 0, time.UTC)) }))
|
||||
|
||||
router.GET("/api/teams/:tid/certificates", apiHandler(teamHandler(
|
||||
func(team fic.Team, _ []byte) (interface{}, error) { return fic.GetTeamCertificates(team) })))
|
||||
|
||||
router.GET("/api/certs/", apiHandler(getCertificates))
|
||||
router.POST("/api/certs/", apiHandler(generateClientCert))
|
||||
router.DELETE("/api/certs/", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) { return fic.ClearCertificates() }))
|
||||
|
||||
router.HEAD("/api/certs/:certid", apiHandler(certificateHandler(getTeamP12File)))
|
||||
router.GET("/api/certs/:certid", apiHandler(certificateHandler(getTeamP12File)))
|
||||
router.PUT("/api/certs/:certid", apiHandler(certificateHandler(updateCertificateAssociation)))
|
||||
router.DELETE("/api/certs/:certid", apiHandler(certificateHandler(
|
||||
func(cert fic.Certificate, _ []byte) (interface{}, error) { return cert.Revoke() })))
|
||||
}
|
||||
|
||||
func infoCA(_ httprouter.Params, _ []byte) (interface{}, error) {
|
||||
_, cacert, err := pki.LoadCA()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := map[string]interface{}{}
|
||||
|
||||
ret["version"] = cacert.Version
|
||||
ret["serialnumber"] = cacert.SerialNumber
|
||||
ret["issuer"] = cacert.Issuer
|
||||
ret["subject"] = cacert.Subject
|
||||
ret["notbefore"] = cacert.NotBefore
|
||||
ret["notafter"] = cacert.NotAfter
|
||||
ret["signatureAlgorithm"] = cacert.SignatureAlgorithm
|
||||
ret["publicKeyAlgorithm"] = cacert.PublicKeyAlgorithm
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func getCAPEM(_ httprouter.Params, _ []byte) (interface{}, error) {
|
||||
if _, err := os.Stat(pki.CACertPath()); os.IsNotExist(err) {
|
||||
return nil, errors.New("Unable to locate the CA root certificate. Have you generated it?")
|
||||
} else if fd, err := os.Open(pki.CACertPath()); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer fd.Close()
|
||||
return ioutil.ReadAll(fd)
|
||||
}
|
||||
}
|
||||
|
||||
func getTeamP12File(cert fic.Certificate, _ []byte) (interface{}, error) {
|
||||
// Create p12 if necessary
|
||||
if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) {
|
||||
if err := pki.WriteP12(cert.Id, cert.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) {
|
||||
return nil, errors.New("Unable to locate the p12. Have you generated it?")
|
||||
} else if fd, err := os.Open(pki.ClientP12Path(cert.Id)); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer fd.Close()
|
||||
return ioutil.ReadAll(fd)
|
||||
}
|
||||
}
|
||||
|
||||
func generateClientCert(_ httprouter.Params, _ []byte) (interface{}, error) {
|
||||
// First, generate a new, unique, serial
|
||||
serial := rand.Int63()
|
||||
for fic.ExistingCertSerial(serial) {
|
||||
serial = rand.Int63()
|
||||
}
|
||||
|
||||
// Let's pick a random password
|
||||
password, err := pki.GeneratePassword()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ok, now load CA
|
||||
capriv, cacert, err := pki.LoadCA()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate our privkey
|
||||
if err := pki.GenerateClient(serial, cacert.NotBefore, cacert.NotAfter, &cacert, &capriv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save in DB
|
||||
return fic.RegisterCertificate(serial, password)
|
||||
}
|
||||
|
||||
type CertExported struct {
|
||||
Id string `json:"id"`
|
||||
Creation time.Time `json:"creation"`
|
||||
IdTeam *int64 `json:"id_team"`
|
||||
Revoked *time.Time `json:"revoked"`
|
||||
}
|
||||
|
||||
func getCertificates(_ httprouter.Params, _ []byte) (interface{}, error) {
|
||||
if certificates, err := fic.GetCertificates(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
ret := make([]CertExported, 0)
|
||||
for _, c := range certificates {
|
||||
ret = append(ret, CertExported{fmt.Sprintf("%d", c.Id), c.Creation, c.IdTeam, c.Revoked})
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type CertUploaded struct {
|
||||
Team *int64 `json:"id_team"`
|
||||
}
|
||||
|
||||
func updateCertificateAssociation(cert fic.Certificate, body []byte) (interface{}, error) {
|
||||
var uc CertUploaded
|
||||
if err := json.Unmarshal(body, &uc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: This should be read from file system, not in DB:
|
||||
// the relation is made through a symlink, so if it exists, it is suffisant to read the relation
|
||||
// moreover, backend doesn't update the DB at registration, it only creates a symlink
|
||||
cert.IdTeam = uc.Team
|
||||
|
||||
dstLinkPath := path.Join(TeamsDir, fmt.Sprintf("_AUTH_ID_%X", cert.Id))
|
||||
|
||||
if uc.Team != nil {
|
||||
srcLinkPath := fmt.Sprintf("%d", *uc.Team)
|
||||
if err := os.Symlink(srcLinkPath, dstLinkPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
os.Remove(dstLinkPath)
|
||||
}
|
||||
|
||||
if _, err := cert.Update(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return cert, err
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ func showClaim(claim fic.Claim, _ []byte) (interface{}, error) {
|
||||
|
||||
type ClaimUploaded struct {
|
||||
Subject string `json:"subject"`
|
||||
Team *int `json:"id_team"`
|
||||
Team *int64 `json:"id_team"`
|
||||
Assignee *int64 `json:"id_assignee"`
|
||||
Priority string `json:"priority"`
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, htt
|
||||
|
||||
func teamPublicHandler(f func(*fic.Team,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
|
||||
return func (ps httprouter.Params, body []byte) (interface{}, error) {
|
||||
if tid, err := strconv.Atoi(string(ps.ByName("tid"))); err != nil {
|
||||
if tid, err := strconv.ParseInt(string(ps.ByName("tid")), 10, 64); err != nil {
|
||||
return nil, err
|
||||
} else if tid == 0 {
|
||||
return f(nil, body)
|
||||
@ -98,7 +98,7 @@ func teamPublicHandler(f func(*fic.Team,[]byte) (interface{}, error)) func (http
|
||||
|
||||
func teamHandler(f func(fic.Team,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
|
||||
return func (ps httprouter.Params, body []byte) (interface{}, error) {
|
||||
if tid, err := strconv.Atoi(string(ps.ByName("tid"))); err != nil {
|
||||
if tid, err := strconv.ParseInt(string(ps.ByName("tid")), 10, 64); err != nil {
|
||||
return nil, err
|
||||
} else if team, err := fic.GetTeam(tid); err != nil {
|
||||
return nil, err
|
||||
@ -280,6 +280,18 @@ func fileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (httprouter
|
||||
}
|
||||
}
|
||||
|
||||
func certificateHandler(f func(fic.Certificate,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
|
||||
return func (ps httprouter.Params, body []byte) (interface{}, error) {
|
||||
if certid, err := strconv.ParseInt(string(ps.ByName("certid")), 10, 64); err != nil {
|
||||
return nil, err
|
||||
} else if cert, err := fic.GetCertificate(certid); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return f(cert, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func notFound(ps httprouter.Params, _ []byte) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ const indextpl = `<!DOCTYPE html>
|
||||
<div class="collapse navbar-collapse" id="adminMenu">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}teams">Équipes</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}pki">PKI</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}themes">Thèmes</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}exercices">Exercices</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}files">Fichiers</a></li>
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"text/template"
|
||||
|
||||
"srs.epita.fr/fic-server/admin/api"
|
||||
"srs.epita.fr/fic-server/admin/pki"
|
||||
"srs.epita.fr/fic-server/admin/sync"
|
||||
"srs.epita.fr/fic-server/libfic"
|
||||
"srs.epita.fr/fic-server/settings"
|
||||
@ -68,6 +69,12 @@ func main() {
|
||||
localImporterSymlink := false
|
||||
|
||||
// Read paremeters from environment
|
||||
if v, exists := os.LookupEnv("FICCA_PASS"); exists {
|
||||
pki.SetCAPassword(v)
|
||||
} else {
|
||||
log.Println("WARNING: no password defined for the CA, will use empty password to secure CA private key")
|
||||
log.Println("WARNING: PLEASE DEFINED ENVIRONMENT VARIABLE: FICCA_PASS")
|
||||
}
|
||||
if v, exists := os.LookupEnv("FICCLOUD_URL"); exists {
|
||||
cloudDAVBase = v
|
||||
}
|
||||
@ -82,6 +89,7 @@ func main() {
|
||||
var bind = flag.String("bind", "127.0.0.1:8081", "Bind port/socket")
|
||||
var dsn = flag.String("dsn", fic.DSNGenerator(), "DSN to connect to the MySQL server")
|
||||
var baseURL = flag.String("baseurl", "/", "URL prepended to each URL")
|
||||
flag.StringVar(&pki.PKIDir, "pki", "./PKI", "Base directory where found PKI scripts")
|
||||
flag.StringVar(&StaticDir, "static", "./htdocs-admin/", "Directory containing static files")
|
||||
flag.StringVar(&api.TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files")
|
||||
flag.StringVar(&settings.SettingsDir, "settings", settings.SettingsDir, "Base directory where load and save settings")
|
||||
@ -122,6 +130,9 @@ func main() {
|
||||
if fic.FilesDir, err = filepath.Abs(fic.FilesDir); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if pki.PKIDir, err = filepath.Abs(pki.PKIDir); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if api.TeamsDir, err = filepath.Abs(api.TeamsDir); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -139,7 +150,7 @@ func main() {
|
||||
baseURL = &tmp
|
||||
}
|
||||
|
||||
log.Println("Opening database...")
|
||||
log.Println("Opening database...")
|
||||
if err := fic.DBInit(*dsn); err != nil {
|
||||
log.Fatal("Cannot open the database: ", err)
|
||||
}
|
||||
|
133
admin/pki/ca.go
Normal file
133
admin/pki/ca.go
Normal file
@ -0,0 +1,133 @@
|
||||
package pki
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
var passwordCA string
|
||||
|
||||
func SetCAPassword(pass string) {
|
||||
passwordCA = pass
|
||||
}
|
||||
|
||||
func CACertPath() string {
|
||||
return path.Join(PKIDir, "shared", "ca.pem")
|
||||
}
|
||||
|
||||
func CAPrivkeyPath() string {
|
||||
return path.Join(PKIDir, "ca.key")
|
||||
}
|
||||
|
||||
func GenerateCA(notBefore time.Time, notAfter time.Time) error {
|
||||
ca := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(0),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"EPITA"},
|
||||
OrganizationalUnit: []string{"SRS laboratory"},
|
||||
Country: []string{"FR"},
|
||||
Locality: []string{"Paris"},
|
||||
CommonName: "FIC CA",
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
IsCA: true,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
// Ensure directories exists
|
||||
os.Mkdir(PKIDir, 0777)
|
||||
os.Mkdir(path.Join(PKIDir, "shared"), 0777)
|
||||
|
||||
pub, priv, err := GeneratePrivKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ca_b, err := x509.CreateCertificate(rand.Reader, ca, ca, pub, priv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save certificate to file
|
||||
if err := saveCertificate(CACertPath(), ca_b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save private key to file
|
||||
if err := savePrivateKeyEncrypted(CAPrivkeyPath(), priv, passwordCA); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadCA() (priv ecdsa.PrivateKey, ca x509.Certificate, err error) {
|
||||
// Load certificate
|
||||
if fd, errr := os.Open(CACertPath()); err != nil {
|
||||
return priv, ca, errr
|
||||
} else {
|
||||
defer fd.Close()
|
||||
if cert, errr := ioutil.ReadAll(fd); err != nil {
|
||||
return priv, ca, errr
|
||||
} else {
|
||||
block, _ := pem.Decode(cert)
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
return priv, ca, errors.New("failed to decode PEM block containing certificate")
|
||||
}
|
||||
if catmp, errr := x509.ParseCertificate(block.Bytes); err != nil {
|
||||
return priv, ca, errr
|
||||
} else if catmp == nil {
|
||||
return priv, ca, errors.New("failed to parse certificate")
|
||||
} else {
|
||||
ca = *catmp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load private key
|
||||
if fd, errr := os.Open(CAPrivkeyPath()); err != nil {
|
||||
return priv, ca, errr
|
||||
} else {
|
||||
defer fd.Close()
|
||||
if privkey, errr := ioutil.ReadAll(fd); err != nil {
|
||||
return priv, ca, errr
|
||||
} else {
|
||||
block, _ := pem.Decode(privkey)
|
||||
if block == nil || block.Type != "EC PRIVATE KEY" {
|
||||
return priv, ca, errors.New("failed to decode PEM block containing EC private key")
|
||||
}
|
||||
|
||||
var decrypted_der []byte
|
||||
if x509.IsEncryptedPEMBlock(block) {
|
||||
decrypted_der, err = x509.DecryptPEMBlock(block, []byte(passwordCA))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
decrypted_der = block.Bytes
|
||||
}
|
||||
|
||||
if tmppriv, errr := x509.ParseECPrivateKey(decrypted_der); err != nil {
|
||||
return priv, ca, errr
|
||||
} else if tmppriv == nil {
|
||||
return priv, ca, errors.New("failed to parse private key")
|
||||
} else {
|
||||
priv = *tmppriv
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
81
admin/pki/client.go
Normal file
81
admin/pki/client.go
Normal file
@ -0,0 +1,81 @@
|
||||
package pki
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ClientCertificatePath(serial int64) string {
|
||||
return path.Join(PKIDir, fmt.Sprintf("%d", serial), "cert.pem")
|
||||
}
|
||||
|
||||
func ClientPrivkeyPath(serial int64) string {
|
||||
return path.Join(PKIDir, fmt.Sprintf("%d", serial), "privkey.pem")
|
||||
}
|
||||
|
||||
func ClientP12Path(serial int64) string {
|
||||
return path.Join(PKIDir, fmt.Sprintf("%d", serial), "team.p12")
|
||||
}
|
||||
|
||||
func GenerateClient(serial int64, notBefore time.Time, notAfter time.Time, parent_cert *x509.Certificate, parent_priv *ecdsa.PrivateKey) error {
|
||||
client := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(serial),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"EPITA"},
|
||||
OrganizationalUnit: []string{"SRS laboratory"},
|
||||
Country: []string{"FR"},
|
||||
Locality: []string{"Paris"},
|
||||
CommonName: fmt.Sprintf("TEAM-%o", serial),
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
IsCA: false,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
pub, priv, err := GeneratePrivKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client_b, err := x509.CreateCertificate(rand.Reader, client, parent_cert, pub, parent_priv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create intermediate directory
|
||||
os.MkdirAll(path.Join(PKIDir, fmt.Sprintf("%d", serial)), 0777)
|
||||
|
||||
// Save certificate to file
|
||||
if err := saveCertificate(ClientCertificatePath(serial), client_b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save private key to file
|
||||
if err := savePrivateKey(ClientPrivkeyPath(serial), priv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteP12(serial int64, password string) error {
|
||||
cmd := exec.Command("/usr/bin/openssl", "pkcs12", "-export",
|
||||
"-inkey", ClientPrivkeyPath(serial),
|
||||
"-in", ClientCertificatePath(serial),
|
||||
"-name", fmt.Sprintf("TEAM-%o", serial),
|
||||
"-passout", "pass:" + password,
|
||||
"-out", ClientP12Path(serial))
|
||||
|
||||
return cmd.Run()
|
||||
}
|
76
admin/pki/common.go
Normal file
76
admin/pki/common.go
Normal file
@ -0,0 +1,76 @@
|
||||
package pki
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func saveCertificate(path string, cert []byte) error {
|
||||
if certOut, err := os.Create(path); err != nil {
|
||||
return err
|
||||
} else {
|
||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert})
|
||||
certOut.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func savePrivateKey(path string, private *ecdsa.PrivateKey) error {
|
||||
if keyOut, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil {
|
||||
return err
|
||||
} else if key_b, err := x509.MarshalECPrivateKey(private); err != nil {
|
||||
return err
|
||||
} else {
|
||||
pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: key_b})
|
||||
keyOut.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func savePrivateKeyEncrypted(path string, private *ecdsa.PrivateKey, password string) error {
|
||||
if password == "" {
|
||||
return savePrivateKey(path, private)
|
||||
}
|
||||
|
||||
if keyOut, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer keyOut.Close()
|
||||
|
||||
if key_b, err := x509.MarshalECPrivateKey(private); err != nil {
|
||||
return err
|
||||
} else if key_c, err := x509.EncryptPEMBlock(rand.Reader, "EC PRIVATE KEY", key_b, []byte(password), x509.PEMCipherAES256); err != nil {
|
||||
return err
|
||||
} else {
|
||||
pem.Encode(keyOut, key_c)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -28,6 +28,9 @@ func init() {
|
||||
api.Router().GET("/public/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
||||
})
|
||||
api.Router().GET("/pki/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
||||
})
|
||||
api.Router().GET("/settings/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
||||
})
|
||||
|
@ -29,6 +29,7 @@
|
||||
<div class="collapse navbar-collapse" id="adminMenu">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="/teams">Équipes</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/pki">PKI</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/themes">Thèmes</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/exercices">Exercices</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/files">Fichiers</a></li>
|
||||
|
@ -17,6 +17,10 @@ angular.module("FICApp", ["ngRoute", "ngResource", "ngSanitize"])
|
||||
controller: "SettingsController",
|
||||
templateUrl: "views/settings.html"
|
||||
})
|
||||
.when("/pki", {
|
||||
controller: "PKIController",
|
||||
templateUrl: "views/pki.html"
|
||||
})
|
||||
.when("/exercices", {
|
||||
controller: "AllExercicesListController",
|
||||
templateUrl: "views/exercice-list.html"
|
||||
@ -132,6 +136,14 @@ angular.module("FICApp")
|
||||
'update': {method: 'PUT'},
|
||||
})
|
||||
})
|
||||
.factory("Certificate", function($resource) {
|
||||
return $resource("/api/certs/:serial", { serial: '@id' }, {
|
||||
'update': {method: 'PUT'},
|
||||
})
|
||||
})
|
||||
.factory("CACertificate", function($resource) {
|
||||
return $resource("/api/ca/:serial", { serial: '@id' })
|
||||
})
|
||||
.factory("File", function($resource) {
|
||||
return $resource("/api/files/:fileId", { fileId: '@id' })
|
||||
})
|
||||
@ -153,6 +165,9 @@ angular.module("FICApp")
|
||||
'update': {method: 'PUT'},
|
||||
})
|
||||
})
|
||||
.factory("TeamCertificate", function($resource) {
|
||||
return $resource("/api/teams/:teamId/certificates", { teamId: '@id' })
|
||||
})
|
||||
.factory("TeamMember", function($resource) {
|
||||
return $resource("/api/teams/:teamId/members", { teamId: '@id' }, {
|
||||
'save': {method: 'PUT'},
|
||||
@ -451,6 +466,56 @@ angular.module("FICApp")
|
||||
};
|
||||
})
|
||||
|
||||
.controller("PKIController", function($scope, $rootScope, Certificate, CACertificate, Team, $location, $http) {
|
||||
$scope.teams = Team.query();
|
||||
$scope.certificates = Certificate.query();
|
||||
$scope.ca = CACertificate.get();
|
||||
|
||||
$scope.revoke = function() {
|
||||
var targetserial = $("#revokeModal").data("certificate");
|
||||
if (targetserial) {
|
||||
Certificate.delete({ serial: targetserial }).$promise.then(
|
||||
function() {
|
||||
$('#revokeModal').modal('hide');
|
||||
$scope.certificates = Certificate.query();
|
||||
}, function(response) {
|
||||
$rootScope.newBox('danger', 'An error occurs when trying to associate certificate:', response.data);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.associate = function() {
|
||||
var targetserial = $("#associationModal").data("certificate");
|
||||
if (!targetserial) return;
|
||||
Certificate.update({ serial: targetserial }, { id_team: $scope.selectedTeam }).$promise.then(
|
||||
function() {
|
||||
$('#associationModal').modal('hide');
|
||||
$scope.certificates = Certificate.query();
|
||||
$scope.selectedTeam = null;
|
||||
}, function(response) {
|
||||
$rootScope.newBox('danger', 'An error occurs when trying to associate certificate:', response.data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.generateCA = function() {
|
||||
$http.post("/api/ca/new").then(function() {
|
||||
$scope.ca = CACertificate.get();
|
||||
}, function(response) {
|
||||
$rootScope.newBox('danger', 'An error occurs when generating CA:', response.data);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.generateCert = function() {
|
||||
$http.post("/api/certs").then(function() {
|
||||
$scope.certificates = Certificate.query();
|
||||
}, function(response) {
|
||||
$rootScope.newBox('danger', 'An error occurs when generating certificate:', response.data);
|
||||
});
|
||||
};
|
||||
})
|
||||
|
||||
.controller("PublicController", function($scope, $rootScope, $routeParams, $location, Scene, Theme, Teams, Exercice) {
|
||||
$scope.screens = [0,1,2,3,4,5,6,7,8,9];
|
||||
$scope.screenid = $routeParams.screenId;
|
||||
@ -1068,47 +1133,26 @@ angular.module("FICApp")
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller("TeamController", function($scope, $rootScope, $location, Team, TeamMember, $routeParams, $http) {
|
||||
.controller("TeamController", function($scope, $rootScope, $location, Team, TeamMember, TeamCertificate, $routeParams, $http) {
|
||||
if ($scope.team && $scope.team.id)
|
||||
$routeParams.teamId = $scope.team.id;
|
||||
$scope.team = Team.get({ teamId: $routeParams.teamId });
|
||||
$scope.fields = ["name", "color"];
|
||||
|
||||
$scope.hasCertificate = false;
|
||||
$http({
|
||||
url: "/api/teams/" + Math.floor($routeParams.teamId) + "/certificate.p12",
|
||||
method: "HEAD",
|
||||
transformResponse: null
|
||||
}).then(function(response) {
|
||||
$scope.hasCertificate = true;
|
||||
}, function(response) {
|
||||
$scope.hasCertificate = false;
|
||||
});
|
||||
$scope.certificates = TeamCertificate.query({ teamId: $routeParams.teamId });
|
||||
|
||||
$scope.generateCertificate = function() {
|
||||
$scope.dissociateCertificate = function(certificate) {
|
||||
$http({
|
||||
url: "/api/teams/" + Math.floor($routeParams.teamId) + "/certificate/generate",
|
||||
method: "POST",
|
||||
transformResponse: null
|
||||
url: "/api/certs/" + certificate.id,
|
||||
method: "PUT",
|
||||
data: {
|
||||
id_team: null
|
||||
}
|
||||
}).then(function(response) {
|
||||
$scope.hasCertificate = true;
|
||||
$rootScope.newBox('success', 'Team certificate successfully generated!');
|
||||
$scope.certificates = TeamCertificate.query({ teamId: $routeParams.teamId });
|
||||
$rootScope.newBox('success', 'Certificate successfully dissociated!');
|
||||
}, function(response) {
|
||||
$rootScope.newBox('danger', 'An error occurs when generating certiticate:', response.data);
|
||||
});
|
||||
}
|
||||
$scope.revokeCertificate = function() {
|
||||
if (!confirm("Are you sure you want to revoke this certificate?"))
|
||||
return false;
|
||||
|
||||
$http({
|
||||
url: "/api/teams/" + Math.floor($routeParams.teamId) + "/certificate.p12",
|
||||
method: "DELETE",
|
||||
transformResponse: null
|
||||
}).then(function(response) {
|
||||
$scope.hasCertificate = false;
|
||||
}, function(response) {
|
||||
$rootScope.newBox('danger', 'An error occurs when revoking the certiticate:', response.data);
|
||||
$rootScope.newBox('danger', 'An error occurs when dissociating certiticate:', response.data);
|
||||
});
|
||||
}
|
||||
|
||||
|
126
admin/static/views/pki.html
Normal file
126
admin/static/views/pki.html
Normal file
@ -0,0 +1,126 @@
|
||||
<h2>
|
||||
Certificats clients
|
||||
<button ng-click="generateCert()" class="float-right btn btn-sm btn-primary" style="margin-right: 10px"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Générer un certificat</button>
|
||||
</h2>
|
||||
|
||||
<p><input type="search" class="form-control" placeholder="Search" ng-model="query" autofocus></p>
|
||||
<table class="table table-hover table-bordered table-striped table-sm">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th>Serial</th>
|
||||
<th>Date de création</th>
|
||||
<th>Équipe</th>
|
||||
<th>Révoqué ?</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="certificate in certificates | filter: query" ng-click="show(certificate.id)">
|
||||
<td>{{ certificate.id }}</td>
|
||||
<td>{{ certificate.creation }}</td>
|
||||
<td ng-if="certificate.id_team">
|
||||
<span ng-repeat="team in teams" ng-if="team.id == certificate.id_team">
|
||||
<a ng-href="teams/{{ team.id }}">{{ team.name }}</a>
|
||||
</span>
|
||||
</td>
|
||||
<td ng-if="!certificate.id_team">
|
||||
<button type="button" class="btn btn-sm btn-primary" data-toggle="modal" data-target="#associationModal" data-certificate="{{ certificate.id }}"><span class="glyphicon glyphicon-link" aria-hidden="true"></span> Associer</button>
|
||||
</td>
|
||||
<td>{{ certificate.revoked }}</td>
|
||||
<td>
|
||||
<a type="button" class="btn btn-sm btn-success" href="api/certs/{{ certificate.id }}" target="_self">Télécharger</a>
|
||||
<button type="button" class="btn btn-sm btn-danger" data-toggle="modal" data-target="#revokeModal" data-certificate="{{ certificate.id }}">Révoquer</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="modal fade" id="revokeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Révocation d'un certificat</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Êtes-vous sûr de vouloir révoquer le certificat ?
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="revoke()">Révoquer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$('#revokeModal').on('shown.bs.modal', function (event) {
|
||||
var button = $(event.relatedTarget);
|
||||
var serial = button.data('certificate');
|
||||
|
||||
var modal = $(this);
|
||||
modal.data('certificate', serial);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="modal fade" id="associationModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Associer le certificat à une équipe</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form ng-submit="associate()">
|
||||
<div class="modal-body row">
|
||||
<label for="tteam" class="col-md-auto col-form-label">Équipe</label>
|
||||
<div class="col-md-auto">
|
||||
<select class="custom-select custom-select-sm" id="tteam" ng-model="selectedTeam" ng-options="t.id as t.name for t in teams"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
|
||||
<button type="submit" class="btn btn-primary">Associer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$('#associationModal').on('shown.bs.modal', function (event) {
|
||||
$('#tteam').trigger('focus');
|
||||
|
||||
var button = $(event.relatedTarget);
|
||||
var serial = button.data('certificate');
|
||||
|
||||
var modal = $(this);
|
||||
modal.data('certificate', serial);
|
||||
});
|
||||
</script>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>
|
||||
Autorité de certification
|
||||
<span class="badge badge-success" ng-if="ca.version">Générée</span>
|
||||
<span class="badge badge-danger" ng-if="!ca.version">Introuvable</span>
|
||||
<button ng-click="generateCA()" class="float-right btn btn-sm btn-primary" ng-if="!ca.version"><span class="glyphicon glyphicon-certificate" aria-hidden="true"></span> Générer</button>
|
||||
</h2>
|
||||
|
||||
<div class="alert alert-info" ng-if="!ca.version">
|
||||
<strong>Aucune CA n'a été générée pour le moment.</strong>
|
||||
</div>
|
||||
|
||||
<dl ng-if="ca.version">
|
||||
<ng-repeat ng-repeat="(k, v) in ca">
|
||||
<dt>{{ k }}</dt>
|
||||
<dd ng-if="v.CommonName">/CN={{ v.CommonName }}/OU={{ v.OrganizationalUnit }}/O={{ v.Organization }}/L={{ v.Locality }}/P={{ v.Province }}/C={{ v.Country }}/</dd>
|
||||
<dd ng-if="!v.CommonName">{{ v }}</dd>
|
||||
</ng-repeat>
|
||||
</dl>
|
@ -69,15 +69,30 @@
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-light">
|
||||
<span class="glyphicon glyphicon-certificate" aria-hidden="true"></span>
|
||||
Certificate
|
||||
<span class="badge badge-success" ng-if="hasCertificate">Generated</span>
|
||||
<span class="badge badge-danger" ng-if="!hasCertificate">Not found</span>
|
||||
Certificates
|
||||
<span class="badge badge-success" ng-if="certificates.length">Generated</span>
|
||||
<span class="badge badge-danger" ng-if="!certificates.length">Not found</span>
|
||||
</div>
|
||||
<div class="card-body bg-light text-dark">
|
||||
<button ng-click="generateCertificate()" class="btn btn-success" ng-if="!hasCertificate">
|
||||
<span class="glyphicon glyphicon-certificate" aria-hidden="true"></span> Generate certificate</button>
|
||||
<button ng-click="revokeCertificate()" class="btn btn-danger" ng-if="hasCertificate">
|
||||
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Revoke certificate</button>
|
||||
<dl class="dl-horizontal" ng-repeat="cert in certificates">
|
||||
<dt>
|
||||
Numéro de série
|
||||
<button type="button" class="btn btn-sm btn-primary float-right" ng-click="dissociateCertificate(cert)">Dissocier</button>
|
||||
</dt>
|
||||
<dd>
|
||||
{{ cert.id }}
|
||||
<span class="badge badge-danger" ng-if="cert.revoked">Révoqué</span>
|
||||
</dd>
|
||||
<dt>
|
||||
Date de création
|
||||
<a class="btn btn-sm btn-success float-right" href="../api/certs/{{ cert.id }}">Télécharger</a>
|
||||
</dt>
|
||||
<dd>{{ cert.creation }}</dd>
|
||||
<dt>Mot de passe</dt>
|
||||
<dd>{{ cert.password }}</dd>
|
||||
<dt ng-if="cert.revoked">Date de révocation</dt>
|
||||
<dd ng-if="cert.revoked">{{ cert.revoked }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -143,15 +143,33 @@ func treat(raw_path string) {
|
||||
if len(spath) == 3 {
|
||||
if spath[1] == "_registration" {
|
||||
treatRegistration(raw_path, spath[2])
|
||||
} else if teamid, err := strconv.Atoi(spath[1]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var team fic.Team
|
||||
|
||||
if strings.HasPrefix(spath[1], "_AUTH_ID_") {
|
||||
if serial, err := strconv.ParseInt(strings.TrimPrefix(spath[1], "_AUTH_ID_"), 16, 64); err != nil {
|
||||
log.Println("[ERR]", err)
|
||||
return
|
||||
} else if team, err = fic.GetTeamBySerial(serial); err != nil {
|
||||
log.Println("[ERR]", err)
|
||||
return
|
||||
}
|
||||
} else if teamid, err := strconv.ParseInt(spath[1], 10, 64); err != nil {
|
||||
log.Println("[ERR]", err)
|
||||
} else if team, err := fic.GetTeam(teamid); err != nil {
|
||||
return
|
||||
} else if team, err = fic.GetTeam(teamid); err != nil {
|
||||
log.Println("[ERR]", err)
|
||||
} else if spath[2] == "name" {
|
||||
return
|
||||
}
|
||||
|
||||
switch spath[2] {
|
||||
case "name":
|
||||
treatRename(raw_path, team)
|
||||
} else if spath[2] == "hint" {
|
||||
case "hint":
|
||||
treatOpeningHint(raw_path, team)
|
||||
} else {
|
||||
default:
|
||||
treatSubmission(raw_path, team, spath[2])
|
||||
}
|
||||
} else {
|
||||
|
@ -43,11 +43,21 @@ func treatRegistration(pathname string, team_id string) {
|
||||
log.Println("[WRN] Unable to create event:", err)
|
||||
}
|
||||
|
||||
teamDirPath := path.Join(TeamsDir, fmt.Sprintf("%d", team.Id))
|
||||
if err := os.MkdirAll(teamDirPath, 0777); err != nil {
|
||||
teamDirPath := fmt.Sprintf("%d", team.Id)
|
||||
|
||||
// Create team directories into TEAMS
|
||||
if err := os.MkdirAll(path.Join(TeamsDir, teamDirPath), 0777); err != nil {
|
||||
log.Println("[ERR]", err)
|
||||
}
|
||||
if err := os.Symlink(teamDirPath, path.Join(TeamsDir, fmt.Sprintf("_AUTH_ID_%s", team_id))); err != nil {
|
||||
if err := os.Symlink(teamDirPath, path.Join(TeamsDir, team_id)); err != nil {
|
||||
log.Println("[ERR]", err)
|
||||
}
|
||||
|
||||
// Create team directories into submissions
|
||||
if err := os.MkdirAll(path.Join(SubmissionDir, teamDirPath), 0777); err != nil {
|
||||
log.Println("[ERR]", err)
|
||||
}
|
||||
if err := os.Symlink(teamDirPath, path.Join(SubmissionDir, team_id)); err != nil {
|
||||
log.Println("[ERR]", err)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,105 @@
|
||||
package fic
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Certificate struct {
|
||||
Id int64 `json:"id,string"`
|
||||
Creation time.Time `json:"creation"`
|
||||
Password string `json:"password"`
|
||||
IdTeam *int64 `json:"id_team"`
|
||||
Revoked *time.Time `json:"revoked"`
|
||||
}
|
||||
|
||||
func GetCertificates() (certificates []Certificate, err error) {
|
||||
var rows *sql.Rows
|
||||
if rows, err = DBQuery("SELECT id_cert, creation, password, id_team, revoked FROM certificates ORDER BY creation"); err == nil {
|
||||
defer rows.Close()
|
||||
|
||||
certificates = make([]Certificate, 0)
|
||||
for rows.Next() {
|
||||
var c Certificate
|
||||
if err = rows.Scan(&c.Id, &c.Creation, &c.Password, &c.IdTeam, &c.Revoked); err != nil {
|
||||
return
|
||||
}
|
||||
certificates = append(certificates, c)
|
||||
}
|
||||
err = rows.Err()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetTeamCertificates(team Team) (certificates []Certificate, err error) {
|
||||
var rows *sql.Rows
|
||||
if rows, err = DBQuery("SELECT id_cert, creation, password, id_team, revoked FROM certificates WHERE id_team = ? ORDER BY creation", team.Id); err == nil {
|
||||
defer rows.Close()
|
||||
|
||||
certificates = make([]Certificate, 0)
|
||||
for rows.Next() {
|
||||
var c Certificate
|
||||
if err = rows.Scan(&c.Id, &c.Creation, &c.Password, &c.IdTeam, &c.Revoked); err != nil {
|
||||
return
|
||||
}
|
||||
certificates = append(certificates, c)
|
||||
}
|
||||
err = rows.Err()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetCertificate(serial int64) (c Certificate, err error) {
|
||||
err = DBQueryRow("SELECT id_cert, creation, password, id_team, revoked FROM certificates WHERE id_cert = ?", serial).Scan(&c.Id, &c.Creation, &c.Password, &c.IdTeam, &c.Revoked)
|
||||
return
|
||||
}
|
||||
|
||||
func ExistingCertSerial(serial int64) (bool) {
|
||||
c, _ := GetCertificate(serial)
|
||||
return c.Id > 0
|
||||
}
|
||||
|
||||
func RegisterCertificate(serial int64, password string) (Certificate, error) {
|
||||
now := time.Now()
|
||||
if _, err := DBExec("INSERT INTO certificates (id_cert, creation, password) VALUES (?, ?, ?)", serial, now, password); err != nil {
|
||||
return Certificate{}, err
|
||||
} else {
|
||||
return Certificate{serial, now, password, nil, nil}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c Certificate) Update() (int64, error) {
|
||||
if res, err := DBExec("UPDATE certificates SET creation = ?, password = ?, id_team = ?, revoked = ? WHERE id_cert = ?", c.Creation, c.Password, c.IdTeam, c.Revoked, c.Id); err != nil {
|
||||
return 0, err
|
||||
} else if nb, err := res.RowsAffected(); err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
return nb, err
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Certificate) Revoke() (int64, error) {
|
||||
now := time.Now()
|
||||
c.Revoked = &now
|
||||
return c.Update()
|
||||
}
|
||||
|
||||
func (c Certificate) Delete() (int64, error) {
|
||||
if res, err := DBExec("DELETE FROM certificates WHERE id_cert = ?", c.Id); err != nil {
|
||||
return 0, err
|
||||
} else if nb, err := res.RowsAffected(); err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
return nb, err
|
||||
}
|
||||
}
|
||||
|
||||
func ClearCertificates() (int64, error) {
|
||||
if res, err := DBExec("DELETE FROM certificates"); err != nil {
|
||||
return 0, err
|
||||
} else if nb, err := res.RowsAffected(); err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
return nb, err
|
||||
}
|
||||
}
|
||||
|
12
libfic/db.go
12
libfic/db.go
@ -79,6 +79,18 @@ CREATE TABLE IF NOT EXISTS teams(
|
||||
name VARCHAR(255) NOT NULL,
|
||||
color INTEGER NOT NULL
|
||||
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
||||
`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS certificates(
|
||||
id_cert BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||
creation TIMESTAMP NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
id_team INTEGER NULL,
|
||||
revoked TIMESTAMP NULL,
|
||||
FOREIGN KEY(id_team) REFERENCES teams(id_team)
|
||||
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
||||
`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ func GetTeams() ([]Team, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func GetTeam(id int) (Team, error) {
|
||||
func GetTeam(id int64) (Team, error) {
|
||||
var t Team
|
||||
if err := DBQueryRow("SELECT id_team, name, color FROM teams WHERE id_team = ?", id).Scan(&t.Id, &t.Name, &t.Color); err != nil {
|
||||
return t, err
|
||||
|
@ -85,7 +85,7 @@ func NewClaim(subject string, team *Team, assignee *ClaimAssignee, priority stri
|
||||
func (c Claim) GetTeam() (*Team, error) {
|
||||
if c.IdTeam == nil {
|
||||
return nil, nil
|
||||
} else if t, err := GetTeam(int(*c.IdTeam)); err != nil {
|
||||
} else if t, err := GetTeam(*c.IdTeam); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &t, nil
|
||||
|
Loading…
Reference in New Issue
Block a user