More work on project permissions

This commit is contained in:
simon987 2019-02-16 11:47:54 -05:00
parent 03153c4d39
commit e079fc8497
18 changed files with 470 additions and 125 deletions

View File

@ -128,6 +128,9 @@ func (api *WebAPI) ProjectCreate(r *Request) {
}, 500) }, 500)
return return
} }
api.Database.SetManagerRoleOn(manager.(*storage.Manager), id,
storage.ROLE_MANAGE_ACCESS|storage.ROLE_READ|storage.ROLE_EDIT)
r.OkJson(CreateProjectResponse{ r.OkJson(CreateProjectResponse{
Ok: true, Ok: true,
Id: id, Id: id,
@ -169,9 +172,33 @@ func (api *WebAPI) ProjectUpdate(r *Request) {
Chain: updateReq.Chain, Chain: updateReq.Chain,
} }
if isValidProject(project) { if !isValidProject(project) {
err := api.Database.UpdateProject(project) logrus.WithFields(logrus.Fields{
"project": project,
}).Warn("Invalid project")
r.Json(CreateProjectResponse{
Ok: false,
Message: "Invalid project",
}, 400)
return
}
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
if !isProjectUpdateAuthorized(project, manager, api.Database) {
r.Json(CreateProjectResponse{
Ok: false,
Message: "Unauthorized",
}, 403)
logrus.WithError(err).WithFields(logrus.Fields{
"project": project,
}).Warn("Unauthorized project update")
return
}
err = api.Database.UpdateProject(project)
if err != nil { if err != nil {
r.Json(CreateProjectResponse{ r.Json(CreateProjectResponse{
Ok: false, Ok: false,
@ -190,17 +217,6 @@ func (api *WebAPI) ProjectUpdate(r *Request) {
"project": project, "project": project,
}).Debug("Updated project") }).Debug("Updated project")
} }
} else {
logrus.WithFields(logrus.Fields{
"project": project,
}).Warn("Invalid project")
r.Json(CreateProjectResponse{
Ok: false,
Message: "Invalid project",
}, 400)
}
} }
func isValidProject(project *storage.Project) bool { func isValidProject(project *storage.Project) bool {
@ -210,18 +226,20 @@ func isValidProject(project *storage.Project) bool {
if project.Priority < 0 { if project.Priority < 0 {
return false return false
} }
if project.Hidden && project.Public {
return false
}
return true return true
} }
func isProjectCreationAuthorized(project *storage.Project, manager interface{}) bool { func isProjectCreationAuthorized(project *storage.Project, manager interface{}) bool {
return true
if manager == nil { if manager == nil {
return false return false
} }
if project.Public && manager.(*storage.Manager).WebsiteAdmin { if project.Public && !manager.(*storage.Manager).WebsiteAdmin {
return false return false
} }
return true return true
@ -229,16 +247,15 @@ func isProjectCreationAuthorized(project *storage.Project, manager interface{})
func isProjectUpdateAuthorized(project *storage.Project, manager interface{}, db *storage.Database) bool { func isProjectUpdateAuthorized(project *storage.Project, manager interface{}, db *storage.Database) bool {
var man storage.Manager if manager == nil {
if manager != nil { return false
man = manager.(storage.Manager)
} }
if man.WebsiteAdmin { if manager.(*storage.Manager).WebsiteAdmin {
return true return true
} }
role := db.ManagerHasRoleOn(&man, project.Id) role := db.GetManagerRoleOn(manager.(*storage.Manager), project.Id)
if role&storage.ROLE_EDIT == 1 { if role&storage.ROLE_EDIT == 1 {
return true return true
} }
@ -246,36 +263,69 @@ func isProjectUpdateAuthorized(project *storage.Project, manager interface{}, db
return false return false
} }
func isProjectReadAuthorized(project *storage.Project, manager interface{}, db *storage.Database) bool {
if project.Public || !project.Hidden {
return true
}
if manager == nil {
return false
}
if manager.(*storage.Manager).WebsiteAdmin {
return true
}
role := db.GetManagerRoleOn(manager.(*storage.Manager), project.Id)
if role&storage.ROLE_READ == 1 {
return true
}
return false
}
func (api *WebAPI) ProjectGet(r *Request) { func (api *WebAPI) ProjectGet(r *Request) {
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64) id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
handleErr(err, r) //todo handle invalid id handleErr(err, r) //todo handle invalid id
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
project := api.Database.GetProject(id) project := api.Database.GetProject(id)
if project != nil { if project == nil {
r.OkJson(GetProjectResponse{
Ok: true,
Project: project,
})
} else {
r.Json(GetProjectResponse{ r.Json(GetProjectResponse{
Ok: false, Ok: false,
Message: "Project not found", Message: "Project not found",
}, 404) }, 404)
return
} }
if !isProjectReadAuthorized(project, manager, api.Database) {
r.Json(GetProjectResponse{
Ok: false,
Message: "Unauthorized",
}, 403)
return
}
r.OkJson(GetProjectResponse{
Ok: true,
Project: project,
})
} }
func (api *WebAPI) ProjectGetAllProjects(r *Request) { func (api *WebAPI) ProjectGetAllProjects(r *Request) {
worker, _ := api.validateSignature(r) sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
var id int64 var id int64
if worker == nil { if manager == nil {
id = 0 id = 0
} else { } else {
id = worker.Id id = manager.(*storage.Manager).Id
} }
projects := api.Database.GetAllProjects(id) projects := api.Database.GetAllProjects(id)
r.OkJson(GetAllProjectsResponse{ r.OkJson(GetAllProjectsResponse{
@ -314,7 +364,7 @@ func (api *WebAPI) ProjectGetAccessRequests(r *Request) {
} }
if !manager.(*storage.Manager).WebsiteAdmin && if !manager.(*storage.Manager).WebsiteAdmin &&
api.Database.ManagerHasRoleOn(manager.(*storage.Manager), 1)& api.Database.GetManagerRoleOn(manager.(*storage.Manager), 1)&
storage.ROLE_MANAGE_ACCESS == 0 { storage.ROLE_MANAGE_ACCESS == 0 {
r.Json(ProjectGetAccessRequestsResponse{ r.Json(ProjectGetAccessRequestsResponse{
Ok: false, Ok: false,

View File

@ -126,7 +126,7 @@ func (database *Database) UpdateManagerPassword(manager *Manager, newPassword []
}).Trace("Database.UpdateManagerPassword UPDATE") }).Trace("Database.UpdateManagerPassword UPDATE")
} }
func (database *Database) ManagerHasRoleOn(manager *Manager, projectId int64) ManagerRole { func (database *Database) GetManagerRoleOn(manager *Manager, projectId int64) ManagerRole {
db := database.getDB() db := database.getDB()
@ -142,6 +142,25 @@ func (database *Database) ManagerHasRoleOn(manager *Manager, projectId int64) Ma
return role return role
} }
func (database *Database) SetManagerRoleOn(manager *Manager, projectId int64, role ManagerRole) {
db := database.getDB()
res, err := db.Exec(`INSERT INTO manager_has_role_on_project (manager, role, project)
VALUES ($1,$2,$3) ON CONFLICT (manager, project) DO UPDATE SET role=$2`,
manager.Id, role, projectId)
handleErr(err)
rowsAffected, _ := res.RowsAffected()
logrus.WithFields(logrus.Fields{
"role": role,
"manager": manager.Username,
"rowsAffected": rowsAffected,
"project": projectId,
}).Info("Set manager role on project")
}
func (database *Database) GetAllManagers() *[]Manager { func (database *Database) GetAllManagers() *[]Manager {
db := database.getDB() db := database.getDB()

View File

@ -127,13 +127,13 @@ func (database *Database) UpdateProject(project *Project) error {
return nil return nil
} }
func (database Database) GetAllProjects(workerId int64) *[]Project { func (database Database) GetAllProjects(managerId int64) *[]Project {
projects := make([]Project, 0) projects := make([]Project, 0)
db := database.getDB() db := database.getDB()
var rows *sql.Rows var rows *sql.Rows
var err error var err error
if workerId == 0 { if managerId == 0 {
rows, err = db.Query(`SELECT rows, err = db.Query(`SELECT
Id, priority, name, clone_url, git_repo, version, motd, public, hidden, COALESCE(chain,0) Id, priority, name, clone_url, git_repo, version, motd, public, hidden, COALESCE(chain,0)
FROM project FROM project
@ -143,9 +143,9 @@ func (database Database) GetAllProjects(workerId int64) *[]Project {
rows, err = db.Query(`SELECT rows, err = db.Query(`SELECT
Id, priority, name, clone_url, git_repo, version, motd, public, hidden, COALESCE(chain,0) Id, priority, name, clone_url, git_repo, version, motd, public, hidden, COALESCE(chain,0)
FROM project FROM project
LEFT JOIN worker_has_access_to_project whatp ON whatp.project = id LEFT JOIN manager_has_role_on_project mhrop ON mhrop.project = id AND mhrop.manager=$1
WHERE NOT hidden OR whatp.worker = $1 WHERE NOT hidden OR mhrop.role & 1 = 1 OR (SELECT tracker_admin FROM manager WHERE id=$1)
ORDER BY name`, workerId) ORDER BY name`, managerId)
} }
handleErr(err) handleErr(err)

View File

@ -1,11 +1,14 @@
package test package test
import ( import (
"bytes"
"encoding/json" "encoding/json"
"github.com/simon987/task_tracker/api" "github.com/simon987/task_tracker/api"
"github.com/simon987/task_tracker/config" "github.com/simon987/task_tracker/config"
"golang.org/x/net/publicsuffix"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/cookiejar"
"testing" "testing"
) )
@ -142,7 +145,7 @@ func TestInvalidCredentialsLogin(t *testing.T) {
func register(request *api.RegisterRequest) *api.RegisterResponse { func register(request *api.RegisterRequest) *api.RegisterResponse {
r := Post("/register", request, nil) r := Post("/register", request, nil, nil)
resp := &api.RegisterResponse{} resp := &api.RegisterResponse{}
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -154,7 +157,7 @@ func register(request *api.RegisterRequest) *api.RegisterResponse {
func login(request *api.LoginRequest) (*api.LoginResponse, *http.Response) { func login(request *api.LoginRequest) (*api.LoginResponse, *http.Response) {
r := Post("/login", request, nil) r := Post("/login", request, nil, nil)
resp := &api.LoginResponse{} resp := &api.LoginResponse{}
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -164,6 +167,34 @@ func login(request *api.LoginRequest) (*api.LoginResponse, *http.Response) {
return resp, r return resp, r
} }
func getSessionCtx(username string, password string, admin bool) { func getSessionCtx(username string, password string, admin bool) *http.Client {
register(&api.RegisterRequest{
Username: username,
Password: password,
})
if admin {
manager, _ := testApi.Database.ValidateCredentials([]byte(username), []byte(password))
manager.WebsiteAdmin = true
testApi.Database.UpdateManager(manager)
}
body, err := json.Marshal(api.LoginRequest{
Username: username,
Password: password,
})
buf := bytes.NewBuffer(body)
req, err := http.NewRequest("POST", "http://"+config.Cfg.ServerAddr+"/login", buf)
handleErr(err)
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
client := &http.Client{
Jar: jar,
}
_, err = client.Do(req)
handleErr(err)
return client
} }

View File

@ -13,7 +13,7 @@ import (
func TestWebHookNoSignature(t *testing.T) { func TestWebHookNoSignature(t *testing.T) {
r := Post("/git/receivehook", api.GitPayload{}, nil) r := Post("/git/receivehook", api.GitPayload{}, nil, nil)
if r.StatusCode != 403 { if r.StatusCode != 403 {
t.Error() t.Error()
@ -35,7 +35,7 @@ func TestWebHookInvalidSignature(t *testing.T) {
func TestWebHookDontUpdateVersion(t *testing.T) { func TestWebHookDontUpdateVersion(t *testing.T) {
resp := createProject(api.CreateProjectRequest{ resp := createProjectAsAdmin(api.CreateProjectRequest{
Name: "My version should not be updated", Name: "My version should not be updated",
Version: "old", Version: "old",
GitRepo: "username/not_this_one", GitRepo: "username/not_this_one",
@ -59,7 +59,7 @@ func TestWebHookDontUpdateVersion(t *testing.T) {
t.Error() t.Error()
} }
getResp, _ := getProject(resp.Id) getResp, _ := getProjectAsAdmin(resp.Id)
if getResp.Project.Version != "old" { if getResp.Project.Version != "old" {
t.Error() t.Error()
@ -67,7 +67,7 @@ func TestWebHookDontUpdateVersion(t *testing.T) {
} }
func TestWebHookUpdateVersion(t *testing.T) { func TestWebHookUpdateVersion(t *testing.T) {
resp := createProject(api.CreateProjectRequest{ resp := createProjectAsAdmin(api.CreateProjectRequest{
Name: "My version should be updated", Name: "My version should be updated",
Version: "old", Version: "old",
GitRepo: "username/repo_name", GitRepo: "username/repo_name",
@ -91,7 +91,7 @@ func TestWebHookUpdateVersion(t *testing.T) {
t.Error() t.Error()
} }
getResp, _ := getProject(resp.Id) getResp, _ := getProjectAsAdmin(resp.Id)
if getResp.Project.Version != "new" { if getResp.Project.Version != "new" {
t.Error() t.Error()

View File

@ -9,7 +9,7 @@ import (
func TestIndex(t *testing.T) { func TestIndex(t *testing.T) {
r := Get("/", nil) r := Get("/", nil, nil)
body, _ := ioutil.ReadAll(r.Body) body, _ := ioutil.ReadAll(r.Body)
var info api.Info var info api.Info

View File

@ -18,7 +18,7 @@ func TestTraceValid(t *testing.T) {
Scope: "test", Scope: "test",
Message: "This is a test message", Message: "This is a test message",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}, w) }, w, nil)
if r.StatusCode != 200 { if r.StatusCode != 200 {
t.Fail() t.Fail()
@ -30,7 +30,7 @@ func TestTraceInvalidScope(t *testing.T) {
r := Post("/log/trace", api.LogRequest{ r := Post("/log/trace", api.LogRequest{
Message: "this is a test message", Message: "this is a test message",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}, w) }, w, nil)
if r.StatusCode == 200 { if r.StatusCode == 200 {
t.Error() t.Error()
@ -40,7 +40,7 @@ func TestTraceInvalidScope(t *testing.T) {
Scope: "", Scope: "",
Message: "this is a test message", Message: "this is a test message",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}, w) }, w, nil)
if r.StatusCode == 200 { if r.StatusCode == 200 {
t.Error() t.Error()
@ -56,7 +56,7 @@ func TestTraceInvalidMessage(t *testing.T) {
Scope: "test", Scope: "test",
Message: "", Message: "",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}, w) }, w, nil)
if r.StatusCode == 200 { if r.StatusCode == 200 {
t.Error() t.Error()
@ -71,7 +71,7 @@ func TestTraceInvalidTime(t *testing.T) {
r := Post("/log/trace", api.LogRequest{ r := Post("/log/trace", api.LogRequest{
Scope: "test", Scope: "test",
Message: "test", Message: "test",
}, w) }, w, nil)
if r.StatusCode == 200 { if r.StatusCode == 200 {
t.Error() t.Error()
} }
@ -87,7 +87,7 @@ func TestWarnValid(t *testing.T) {
Scope: "test", Scope: "test",
Message: "test", Message: "test",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}, w) }, w, nil)
if r.StatusCode != 200 { if r.StatusCode != 200 {
t.Fail() t.Fail()
} }
@ -100,7 +100,7 @@ func TestInfoValid(t *testing.T) {
Scope: "test", Scope: "test",
Message: "test", Message: "test",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}, w) }, w, nil)
if r.StatusCode != 200 { if r.StatusCode != 200 {
t.Fail() t.Fail()
} }
@ -113,7 +113,7 @@ func TestErrorValid(t *testing.T) {
Scope: "test", Scope: "test",
Message: "test", Message: "test",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}, w) }, w, nil)
if r.StatusCode != 200 { if r.StatusCode != 200 {
t.Fail() t.Fail()
} }
@ -179,7 +179,7 @@ func getLogs(since int64, level storage.LogLevel) *api.GetLogResponse {
r := Post(fmt.Sprintf("/logs"), api.GetLogRequest{ r := Post(fmt.Sprintf("/logs"), api.GetLogRequest{
Since: since, Since: since,
Level: level, Level: level,
}, nil) }, nil, nil)
resp := &api.GetLogResponse{} resp := &api.GetLogResponse{}
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)

View File

@ -11,7 +11,7 @@ import (
func TestCreateGetProject(t *testing.T) { func TestCreateGetProject(t *testing.T) {
resp := createProject(api.CreateProjectRequest{ resp := createProjectAsAdmin(api.CreateProjectRequest{
Name: "Test name", Name: "Test name",
CloneUrl: "http://github.com/test/test", CloneUrl: "http://github.com/test/test",
GitRepo: "drone/webhooktest", GitRepo: "drone/webhooktest",
@ -19,7 +19,7 @@ func TestCreateGetProject(t *testing.T) {
Priority: 123, Priority: 123,
Motd: "motd", Motd: "motd",
Public: true, Public: true,
Hidden: true, Hidden: false,
}) })
id := resp.Id id := resp.Id
@ -31,7 +31,7 @@ func TestCreateGetProject(t *testing.T) {
t.Fail() t.Fail()
} }
getResp, _ := getProject(id) getResp, _ := getProjectAsAdmin(id)
if getResp.Project.Id != id { if getResp.Project.Id != id {
t.Error() t.Error()
@ -60,13 +60,13 @@ func TestCreateGetProject(t *testing.T) {
if getResp.Project.Public != true { if getResp.Project.Public != true {
t.Error() t.Error()
} }
if getResp.Project.Hidden != true { if getResp.Project.Hidden != false {
t.Error() t.Error()
} }
} }
func TestCreateProjectInvalid(t *testing.T) { func TestCreateProjectInvalid(t *testing.T) {
resp := createProject(api.CreateProjectRequest{}) resp := createProjectAsAdmin(api.CreateProjectRequest{})
if resp.Ok != false { if resp.Ok != false {
t.Fail() t.Fail()
@ -74,10 +74,10 @@ func TestCreateProjectInvalid(t *testing.T) {
} }
func TestCreateDuplicateProjectName(t *testing.T) { func TestCreateDuplicateProjectName(t *testing.T) {
createProject(api.CreateProjectRequest{ createProjectAsAdmin(api.CreateProjectRequest{
Name: "duplicate name", Name: "duplicate name",
}) })
resp := createProject(api.CreateProjectRequest{ resp := createProjectAsAdmin(api.CreateProjectRequest{
Name: "duplicate name", Name: "duplicate name",
}) })
@ -91,11 +91,11 @@ func TestCreateDuplicateProjectName(t *testing.T) {
} }
func TestCreateDuplicateProjectRepo(t *testing.T) { func TestCreateDuplicateProjectRepo(t *testing.T) {
createProject(api.CreateProjectRequest{ createProjectAsAdmin(api.CreateProjectRequest{
Name: "different name", Name: "different name",
GitRepo: "user/same", GitRepo: "user/same",
}) })
resp := createProject(api.CreateProjectRequest{ resp := createProjectAsAdmin(api.CreateProjectRequest{
Name: "but same repo", Name: "but same repo",
GitRepo: "user/same", GitRepo: "user/same",
}) })
@ -111,7 +111,7 @@ func TestCreateDuplicateProjectRepo(t *testing.T) {
func TestGetProjectNotFound(t *testing.T) { func TestGetProjectNotFound(t *testing.T) {
getResp, r := getProject(12345) getResp, r := getProjectAsAdmin(12345)
if getResp.Ok != false { if getResp.Ok != false {
t.Fail() t.Fail()
@ -128,7 +128,7 @@ func TestGetProjectNotFound(t *testing.T) {
func TestUpdateProjectValid(t *testing.T) { func TestUpdateProjectValid(t *testing.T) {
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Public: true, Public: true,
Version: "versionA", Version: "versionA",
Motd: "MotdA", Motd: "MotdA",
@ -146,13 +146,13 @@ func TestUpdateProjectValid(t *testing.T) {
Motd: "MotdB", Motd: "MotdB",
Public: false, Public: false,
Hidden: true, Hidden: true,
}, pid) }, pid, testAdminCtx)
if updateResp.Ok != true { if updateResp.Ok != true {
t.Error() t.Error()
} }
proj, _ := getProject(pid) proj, _ := getProjectAsAdmin(pid)
if proj.Project.Public != false { if proj.Project.Public != false {
t.Error() t.Error()
@ -176,7 +176,7 @@ func TestUpdateProjectValid(t *testing.T) {
func TestUpdateProjectInvalid(t *testing.T) { func TestUpdateProjectInvalid(t *testing.T) {
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Public: true, Public: true,
Version: "lllllllllllll", Version: "lllllllllllll",
Motd: "2wwwwwwwwwwwwwww", Motd: "2wwwwwwwwwwwwwww",
@ -193,7 +193,7 @@ func TestUpdateProjectInvalid(t *testing.T) {
Name: "NameB-0", Name: "NameB-0",
Motd: "MotdB000000", Motd: "MotdB000000",
Public: false, Public: false,
}, pid) }, pid, testAdminCtx)
if updateResp.Ok != false { if updateResp.Ok != false {
t.Error() t.Error()
@ -206,7 +206,7 @@ func TestUpdateProjectInvalid(t *testing.T) {
func TestUpdateProjectConstraintFail(t *testing.T) { func TestUpdateProjectConstraintFail(t *testing.T) {
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Public: true, Public: true,
Version: "testUpdateProjectConstraintFail", Version: "testUpdateProjectConstraintFail",
Motd: "testUpdateProjectConstraintFail", Motd: "testUpdateProjectConstraintFail",
@ -216,7 +216,7 @@ func TestUpdateProjectConstraintFail(t *testing.T) {
Priority: 1, Priority: 1,
}).Id }).Id
createProject(api.CreateProjectRequest{ createProjectAsAdmin(api.CreateProjectRequest{
Public: true, Public: true,
Version: "testUpdateProjectConstraintFail_d", Version: "testUpdateProjectConstraintFail_d",
Motd: "testUpdateProjectConstraintFail_d", Motd: "testUpdateProjectConstraintFail_d",
@ -232,7 +232,7 @@ func TestUpdateProjectConstraintFail(t *testing.T) {
CloneUrl: "testUpdateProjectConstraintFail_d", CloneUrl: "testUpdateProjectConstraintFail_d",
Name: "testUpdateProjectConstraintFail_d", Name: "testUpdateProjectConstraintFail_d",
Motd: "testUpdateProjectConstraintFail_d", Motd: "testUpdateProjectConstraintFail_d",
}, pid) }, pid, testAdminCtx)
if updateResp.Ok != false { if updateResp.Ok != false {
t.Error() t.Error()
@ -243,9 +243,215 @@ func TestUpdateProjectConstraintFail(t *testing.T) {
} }
} }
func createProject(req api.CreateProjectRequest) *api.CreateProjectResponse { func TestNotLoggedProjectCreate(t *testing.T) {
r := Post("/project/create", req, nil) r := createProject(api.CreateProjectRequest{
Hidden: false,
Name: "testnotlogged",
Priority: 1,
CloneUrl: "testnotlogged",
GitRepo: "testnotlogged",
}, nil)
if r.Ok != false {
t.Error()
}
if len(r.Message) <= 0 {
t.Error()
}
}
func TestUserCanCreatePrivateProject(t *testing.T) {
r := createProject(api.CreateProjectRequest{
Hidden: false,
Name: "testuserprivate",
Priority: 1,
CloneUrl: "testuserprivate",
GitRepo: "testuserprivate",
Public: false,
}, testUserCtx)
if r.Ok != true {
t.Error()
}
}
func TestUserCannotCreatePublicProject(t *testing.T) {
r := createProject(api.CreateProjectRequest{
Hidden: false,
Name: "testuserprivate",
Priority: 1,
CloneUrl: "testuserprivate",
GitRepo: "testuserprivate",
Public: true,
}, testUserCtx)
if r.Ok != false {
t.Error()
}
if len(r.Message) <= 0 {
t.Error()
}
}
func TestHiddenProjectsNotShownInList(t *testing.T) {
r := createProject(api.CreateProjectRequest{
Hidden: true,
Name: "testhiddenprojectlist",
Priority: 1,
CloneUrl: "testhiddenprojectlist",
GitRepo: "testhiddenprojectlist",
Public: false,
}, testUserCtx)
if r.Ok != true {
t.Error()
}
list := getProjectList(nil)
for _, p := range *list.Projects {
if p.Id == r.Id {
t.Error()
}
}
}
func TestHiddenProjectCannotBePublic(t *testing.T) {
r := createProject(api.CreateProjectRequest{
Hidden: true,
Name: "testhiddencannotbepublic",
Priority: 1,
CloneUrl: "testhiddencannotbepublic",
GitRepo: "testhiddencannotbepublic",
Public: true,
}, testAdminCtx)
if r.Ok != false {
t.Error()
}
if len(r.Message) <= 0 {
t.Error()
}
}
func TestHiddenProjectNotAccessible(t *testing.T) {
otherUser := getSessionCtx("otheruser", "otheruser", false)
r := createProject(api.CreateProjectRequest{
Hidden: true,
Name: "testhiddenprojectaccess",
Priority: 1,
CloneUrl: "testhiddenprojectaccess",
GitRepo: "testhiddenprojectaccess",
Public: false,
}, testUserCtx)
if r.Ok != true {
t.Error()
}
pAdmin, _ := getProject(r.Id, testAdminCtx)
pUser, _ := getProject(r.Id, testUserCtx)
pOtherUser, _ := getProject(r.Id, otherUser)
pGuest, _ := getProject(r.Id, nil)
if pAdmin.Ok != true {
t.Error()
}
if pUser.Ok != true {
t.Error()
}
if pOtherUser.Ok != false {
t.Error()
}
if pGuest.Ok != false {
t.Error()
}
}
func TestUpdateProjectPermissions(t *testing.T) {
p := createProjectAsAdmin(api.CreateProjectRequest{
GitRepo: "updateprojectpermissions",
CloneUrl: "updateprojectpermissions",
Name: "updateprojectpermissions",
Version: "updateprojectpermissions",
})
r := updateProject(api.UpdateProjectRequest{
GitRepo: "newupdateprojectpermissions",
CloneUrl: "newupdateprojectpermissions",
Name: "newupdateprojectpermissions",
}, p.Id, nil)
if r.Ok != false {
t.Error()
}
if len(r.Message) <= 0 {
t.Error()
}
}
func TestUserWithReadAccessShouldSeeHiddenProjectInList(t *testing.T) {
pHidden := createProject(api.CreateProjectRequest{
GitRepo: "testUserHidden",
CloneUrl: "testUserHidden",
Name: "testUserHidden",
Version: "testUserHidden",
Hidden: true,
}, testUserCtx)
list := getProjectList(testUserCtx)
found := false
for _, p := range *list.Projects {
if p.Id == pHidden.Id {
found = true
}
}
if !found {
t.Error()
}
}
func TestAdminShouldSeeHiddenProjectInList(t *testing.T) {
pHidden := createProject(api.CreateProjectRequest{
GitRepo: "testAdminHidden",
CloneUrl: "testAdminHidden",
Name: "testAdminHidden",
Version: "testAdminHidden",
Hidden: true,
}, testUserCtx)
list := getProjectList(testAdminCtx)
found := false
for _, p := range *list.Projects {
if p.Id == pHidden.Id {
found = true
}
}
if !found {
t.Error()
}
}
func createProjectAsAdmin(req api.CreateProjectRequest) *api.CreateProjectResponse {
return createProject(req, testAdminCtx)
}
func createProject(req api.CreateProjectRequest, s *http.Client) *api.CreateProjectResponse {
r := Post("/project/create", req, nil, s)
var resp api.CreateProjectResponse var resp api.CreateProjectResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -255,9 +461,13 @@ func createProject(req api.CreateProjectRequest) *api.CreateProjectResponse {
return &resp return &resp
} }
func getProject(id int64) (*api.GetProjectResponse, *http.Response) { func getProjectAsAdmin(id int64) (*api.GetProjectResponse, *http.Response) {
return getProject(id, testAdminCtx)
}
r := Get(fmt.Sprintf("/project/get/%d", id), nil) func getProject(id int64, s *http.Client) (*api.GetProjectResponse, *http.Response) {
r := Get(fmt.Sprintf("/project/get/%d", id), nil, s)
var getResp api.GetProjectResponse var getResp api.GetProjectResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -267,9 +477,9 @@ func getProject(id int64) (*api.GetProjectResponse, *http.Response) {
return &getResp, r return &getResp, r
} }
func updateProject(request api.UpdateProjectRequest, pid int64) *api.UpdateProjectResponse { func updateProject(request api.UpdateProjectRequest, pid int64, s *http.Client) *api.UpdateProjectResponse {
r := Post(fmt.Sprintf("/project/update/%d", pid), request, nil) r := Post(fmt.Sprintf("/project/update/%d", pid), request, nil, s)
var resp api.UpdateProjectResponse var resp api.UpdateProjectResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -278,3 +488,14 @@ func updateProject(request api.UpdateProjectRequest, pid int64) *api.UpdateProje
return &resp return &resp
} }
func getProjectList(s *http.Client) *api.GetAllProjectsResponse {
r := Get("/project/list", nil, s)
var resp api.GetAllProjectsResponse
data, _ := ioutil.ReadAll(r.Body)
err := json.Unmarshal(data, &resp)
handleErr(err)
return &resp
}

View File

@ -10,7 +10,7 @@ import (
func BenchmarkCreateTaskRemote(b *testing.B) { func BenchmarkCreateTaskRemote(b *testing.B) {
resp := createProject(api.CreateProjectRequest{ resp := createProjectAsAdmin(api.CreateProjectRequest{
Name: "BenchmarkCreateTask" + strconv.Itoa(b.N), Name: "BenchmarkCreateTask" + strconv.Itoa(b.N),
GitRepo: "benchmark_test" + strconv.Itoa(b.N), GitRepo: "benchmark_test" + strconv.Itoa(b.N),
Version: "f09e8c9r0w839x0c43", Version: "f09e8c9r0w839x0c43",

View File

@ -12,7 +12,7 @@ import (
func TestCreateTaskValid(t *testing.T) { func TestCreateTaskValid(t *testing.T) {
//Make sure there is always a project for id:1 //Make sure there is always a project for id:1
createProject(api.CreateProjectRequest{ createProjectAsAdmin(api.CreateProjectRequest{
Name: "Some Test name", Name: "Some Test name",
Version: "Test Version", Version: "Test Version",
CloneUrl: "http://github.com/test/test", CloneUrl: "http://github.com/test/test",
@ -133,7 +133,7 @@ func TestCreateTaskInvalidRecipe(t *testing.T) {
func TestCreateGetTask(t *testing.T) { func TestCreateGetTask(t *testing.T) {
//Make sure there is always a project for id:1 //Make sure there is always a project for id:1
resp := createProject(api.CreateProjectRequest{ resp := createProjectAsAdmin(api.CreateProjectRequest{
Name: "My project", Name: "My project",
Version: "1.0", Version: "1.0",
CloneUrl: "http://github.com/test/test", CloneUrl: "http://github.com/test/test",
@ -194,7 +194,7 @@ func TestCreateGetTask(t *testing.T) {
func createTasks(prefix string) (int64, int64) { func createTasks(prefix string) (int64, int64) {
lowP := createProject(api.CreateProjectRequest{ lowP := createProjectAsAdmin(api.CreateProjectRequest{
Name: prefix + "low", Name: prefix + "low",
Version: "1.0", Version: "1.0",
CloneUrl: "http://github.com/test/test", CloneUrl: "http://github.com/test/test",
@ -202,7 +202,7 @@ func createTasks(prefix string) (int64, int64) {
Priority: 1, Priority: 1,
Public: true, Public: true,
}) })
highP := createProject(api.CreateProjectRequest{ highP := createProjectAsAdmin(api.CreateProjectRequest{
Name: prefix + "high", Name: prefix + "high",
Version: "1.0", Version: "1.0",
CloneUrl: "http://github.com/test/test", CloneUrl: "http://github.com/test/test",
@ -293,7 +293,7 @@ func TestTaskNoAccess(t *testing.T) {
worker := genWid() worker := genWid()
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Name: "This is a private proj", Name: "This is a private proj",
Motd: "private", Motd: "private",
Version: "private", Version: "private",
@ -335,7 +335,7 @@ func TestTaskHasAccess(t *testing.T) {
worker := genWid() worker := genWid()
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Name: "This is a private proj1", Name: "This is a private proj1",
Motd: "private1", Motd: "private1",
Version: "private1", Version: "private1",
@ -382,7 +382,7 @@ func TestReleaseTaskSuccess(t *testing.T) {
worker := genWid() worker := genWid()
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Priority: 0, Priority: 0,
GitRepo: "testreleasetask", GitRepo: "testreleasetask",
CloneUrl: "lllllllll", CloneUrl: "lllllllll",
@ -420,7 +420,7 @@ func TestReleaseTaskSuccess(t *testing.T) {
func TestCreateIntCollision(t *testing.T) { func TestCreateIntCollision(t *testing.T) {
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Priority: 1, Priority: 1,
GitRepo: "testcreateintcollision", GitRepo: "testcreateintcollision",
CloneUrl: "testcreateintcollision", CloneUrl: "testcreateintcollision",
@ -460,7 +460,7 @@ func TestCreateIntCollision(t *testing.T) {
func TestCreateStringCollision(t *testing.T) { func TestCreateStringCollision(t *testing.T) {
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Priority: 1, Priority: 1,
GitRepo: "testcreatestringcollision", GitRepo: "testcreatestringcollision",
CloneUrl: "testcreatestringcollision", CloneUrl: "testcreatestringcollision",
@ -509,7 +509,7 @@ func TestCreateStringCollision(t *testing.T) {
func TestCannotVerifySameTaskTwice(t *testing.T) { func TestCannotVerifySameTaskTwice(t *testing.T) {
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Priority: 1, Priority: 1,
GitRepo: "verifysametasktwice", GitRepo: "verifysametasktwice",
CloneUrl: "verifysametasktwice", CloneUrl: "verifysametasktwice",
@ -547,7 +547,7 @@ func TestCannotVerifySameTaskTwice(t *testing.T) {
func TestVerification2(t *testing.T) { func TestVerification2(t *testing.T) {
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Priority: 1, Priority: 1,
GitRepo: "verify2", GitRepo: "verify2",
CloneUrl: "verify2", CloneUrl: "verify2",
@ -603,7 +603,7 @@ func TestVerification2(t *testing.T) {
func TestReleaseTaskFail(t *testing.T) { func TestReleaseTaskFail(t *testing.T) {
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Priority: 1, Priority: 1,
GitRepo: "releasefail", GitRepo: "releasefail",
CloneUrl: "releasefail", CloneUrl: "releasefail",
@ -643,14 +643,14 @@ func TestTaskChain(t *testing.T) {
w := genWid() w := genWid()
p1 := createProject(api.CreateProjectRequest{ p1 := createProjectAsAdmin(api.CreateProjectRequest{
Name: "testtaskchain1", Name: "testtaskchain1",
Public: true, Public: true,
GitRepo: "testtaskchain1", GitRepo: "testtaskchain1",
CloneUrl: "testtaskchain1", CloneUrl: "testtaskchain1",
}).Id }).Id
p2 := createProject(api.CreateProjectRequest{ p2 := createProjectAsAdmin(api.CreateProjectRequest{
Name: "testtaskchain2", Name: "testtaskchain2",
Public: true, Public: true,
GitRepo: "testtaskchain2", GitRepo: "testtaskchain2",
@ -692,7 +692,7 @@ func TestTaskChain(t *testing.T) {
func createTask(request api.CreateTaskRequest, worker *storage.Worker) *api.CreateTaskResponse { func createTask(request api.CreateTaskRequest, worker *storage.Worker) *api.CreateTaskResponse {
r := Post("/task/create", request, worker) r := Post("/task/create", request, worker, nil)
var resp api.CreateTaskResponse var resp api.CreateTaskResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -704,7 +704,7 @@ func createTask(request api.CreateTaskRequest, worker *storage.Worker) *api.Crea
func getTask(worker *storage.Worker) *api.GetTaskResponse { func getTask(worker *storage.Worker) *api.GetTaskResponse {
r := Get(fmt.Sprintf("/task/get"), worker) r := Get(fmt.Sprintf("/task/get"), worker, nil)
var resp api.GetTaskResponse var resp api.GetTaskResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -716,7 +716,7 @@ func getTask(worker *storage.Worker) *api.GetTaskResponse {
func getTaskFromProject(project int64, worker *storage.Worker) *api.GetTaskResponse { func getTaskFromProject(project int64, worker *storage.Worker) *api.GetTaskResponse {
r := Get(fmt.Sprintf("/task/get/%d", project), worker) r := Get(fmt.Sprintf("/task/get/%d", project), worker, nil)
var resp api.GetTaskResponse var resp api.GetTaskResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -728,7 +728,7 @@ func getTaskFromProject(project int64, worker *storage.Worker) *api.GetTaskRespo
func releaseTask(request api.ReleaseTaskRequest, worker *storage.Worker) *api.ReleaseTaskResponse { func releaseTask(request api.ReleaseTaskRequest, worker *storage.Worker) *api.ReleaseTaskResponse {
r := Post("/task/release", request, worker) r := Post("/task/release", request, worker, nil)
var resp api.ReleaseTaskResponse var resp api.ReleaseTaskResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)

View File

@ -95,7 +95,7 @@ func TestRemoveAccessFailedProjectConstraint(t *testing.T) {
func TestRemoveAccessFailedWorkerConstraint(t *testing.T) { func TestRemoveAccessFailedWorkerConstraint(t *testing.T) {
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Priority: 1, Priority: 1,
GitRepo: "dfffffffffff", GitRepo: "dfffffffffff",
CloneUrl: "fffffffffff23r", CloneUrl: "fffffffffff23r",
@ -117,7 +117,7 @@ func TestRemoveAccessFailedWorkerConstraint(t *testing.T) {
func TestGrantAccessFailedWorkerConstraint(t *testing.T) { func TestGrantAccessFailedWorkerConstraint(t *testing.T) {
pid := createProject(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Priority: 1, Priority: 1,
GitRepo: "dfffffffffff1", GitRepo: "dfffffffffff1",
CloneUrl: "fffffffffff23r1", CloneUrl: "fffffffffff23r1",
@ -173,7 +173,7 @@ func TestCreateWorkerAliasInvalid(t *testing.T) {
} }
func createWorker(req api.CreateWorkerRequest) (*api.CreateWorkerResponse, *http.Response) { func createWorker(req api.CreateWorkerRequest) (*api.CreateWorkerResponse, *http.Response) {
r := Post("/worker/create", req, nil) r := Post("/worker/create", req, nil, nil)
var resp *api.CreateWorkerResponse var resp *api.CreateWorkerResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -185,7 +185,7 @@ func createWorker(req api.CreateWorkerRequest) (*api.CreateWorkerResponse, *http
func getWorker(id int64) (*api.GetWorkerResponse, *http.Response) { func getWorker(id int64) (*api.GetWorkerResponse, *http.Response) {
r := Get(fmt.Sprintf("/worker/get/%d", id), nil) r := Get(fmt.Sprintf("/worker/get/%d", id), nil, nil)
var resp *api.GetWorkerResponse var resp *api.GetWorkerResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -206,7 +206,7 @@ func grantAccess(wid int64, project int64) *api.WorkerAccessResponse {
r := Post("/access/grant", api.WorkerAccessRequest{ r := Post("/access/grant", api.WorkerAccessRequest{
WorkerId: wid, WorkerId: wid,
ProjectId: project, ProjectId: project,
}, nil) }, nil, nil)
var resp *api.WorkerAccessResponse var resp *api.WorkerAccessResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -221,7 +221,7 @@ func removeAccess(wid int64, project int64) *api.WorkerAccessResponse {
r := Post("/access/remove", api.WorkerAccessRequest{ r := Post("/access/remove", api.WorkerAccessRequest{
WorkerId: wid, WorkerId: wid,
ProjectId: project, ProjectId: project,
}, nil) }, nil, nil)
var resp *api.WorkerAccessResponse var resp *api.WorkerAccessResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -233,7 +233,7 @@ func removeAccess(wid int64, project int64) *api.WorkerAccessResponse {
func updateWorker(request api.UpdateWorkerRequest, w *storage.Worker) *api.UpdateWorkerResponse { func updateWorker(request api.UpdateWorkerRequest, w *storage.Worker) *api.UpdateWorkerResponse {
r := Post("/worker/update", request, w) r := Post("/worker/update", request, w, nil)
var resp *api.UpdateWorkerResponse var resp *api.UpdateWorkerResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)

View File

@ -19,7 +19,11 @@ type SessionContext struct {
SessionCookie *http.Cookie SessionCookie *http.Cookie
} }
func Post(path string, x interface{}, worker *storage.Worker) *http.Response { func Post(path string, x interface{}, worker *storage.Worker, s *http.Client) *http.Response {
if s == nil {
s = &http.Client{}
}
body, err := json.Marshal(x) body, err := json.Marshal(x)
buf := bytes.NewBuffer(body) buf := bytes.NewBuffer(body)
@ -36,14 +40,17 @@ func Post(path string, x interface{}, worker *storage.Worker) *http.Response {
req.Header.Add("X-Signature", sig) req.Header.Add("X-Signature", sig)
} }
client := http.Client{} r, err := s.Do(req)
r, err := client.Do(req)
handleErr(err) handleErr(err)
return r return r
} }
func Get(path string, worker *storage.Worker) *http.Response { func Get(path string, worker *storage.Worker, s *http.Client) *http.Response {
if s == nil {
s = &http.Client{}
}
url := "http://" + config.Cfg.ServerAddr + path url := "http://" + config.Cfg.ServerAddr + path
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
@ -58,8 +65,7 @@ func Get(path string, worker *storage.Worker) *http.Response {
req.Header.Add("X-Signature", sig) req.Header.Add("X-Signature", sig)
} }
client := http.Client{} r, err := s.Do(req)
r, err := client.Do(req)
handleErr(err) handleErr(err)
return r return r

View File

@ -3,19 +3,28 @@ package test
import ( import (
"github.com/simon987/task_tracker/api" "github.com/simon987/task_tracker/api"
"github.com/simon987/task_tracker/config" "github.com/simon987/task_tracker/config"
"net/http"
"testing" "testing"
"time" "time"
) )
var testApi *api.WebAPI
var testAdminCtx *http.Client
var testUserCtx *http.Client
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
config.SetupConfig() config.SetupConfig()
testApi := api.New() testApi = api.New()
testApi.SetupLogger() testApi.SetupLogger()
testApi.Database.Reset() testApi.Database.Reset()
go testApi.Run() go testApi.Run()
time.Sleep(time.Millisecond * 100) time.Sleep(time.Millisecond * 100)
testAdminCtx = getSessionCtx("testadmin", "testadmin", true)
testUserCtx = getSessionCtx("testuser", "testuser", false)
m.Run() m.Run()
} }

View File

@ -79,8 +79,9 @@ CREATE TABLE manager
CREATE TABLE manager_has_role_on_project CREATE TABLE manager_has_role_on_project
( (
manager INTEGER REFERENCES manager (id) NOT NULL, manager INTEGER REFERENCES manager (id) NOT NULL,
role SMALLINT NOT NULl, role SMALLINT NOT NULL,
project INTEGER REFERENCES project (id) NOT NULL project INTEGER REFERENCES project (id) NOT NULL,
primary key (manager, project)
); );
CREATE TABLE project_monitoring_snapshot CREATE TABLE project_monitoring_snapshot

View File

@ -8,4 +8,5 @@ export interface Project {
version: string; version: string;
public: boolean; public: boolean;
chain: number; chain: number;
hidden: boolean;
} }

View File

@ -13,8 +13,11 @@
<mat-expansion-panel *ngFor="let project of projects" style="margin-top: 1em"> <mat-expansion-panel *ngFor="let project of projects" style="margin-top: 1em">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon *ngIf="project.public">public</mat-icon> <mat-icon *ngIf="project.public" [title]="'project.public' | translate">public</mat-icon>
<mat-icon *ngIf="!project.public">lock</mat-icon> <mat-icon *ngIf="!project.public && !project.hidden" [title]="'project.private'|translate">
lock
</mat-icon>
<mat-icon *ngIf="project.hidden" [title]="'project.hidden'|translate">block</mat-icon>
<span style="width: 3em">{{project.id}}</span>{{project.name}} <span style="width: 3em">{{project.id}}</span>{{project.name}}
</mat-panel-title> </mat-panel-title>
<mat-panel-description>{{project.motd}}</mat-panel-description> <mat-panel-description>{{project.motd}}</mat-panel-description>

View File

@ -58,6 +58,8 @@
"create_title": "Create new project", "create_title": "Create new project",
"create_subtitle": "Todo: subtitle", "create_subtitle": "Todo: subtitle",
"public": "Public", "public": "Public",
"private": "Private",
"hidden": "Hidden",
"create": "Create", "create": "Create",
"git_repo": "Git repository name", "git_repo": "Git repository name",
"motd": "Message of the day", "motd": "Message of the day",

View File

@ -60,6 +60,8 @@
"create_title": "Créer un nouveau projet", "create_title": "Créer un nouveau projet",
"create_subtitle": "todo: sous-titre", "create_subtitle": "todo: sous-titre",
"public": "Publique", "public": "Publique",
"private": "Privé",
"hidden": "Caché",
"create": "Créer", "create": "Créer",
"motd": "Message du jour", "motd": "Message du jour",
"update": "Mettre à jour", "update": "Mettre à jour",