Some work on permissions (lacks tests)

This commit is contained in:
simon987 2019-02-13 21:54:18 -05:00
parent 4edf354f8d
commit c3e5bd77f7
34 changed files with 650 additions and 273 deletions

View File

@ -145,7 +145,6 @@ func (api *WebAPI) AccountDetails(r *Request) {
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"manager": manager, "manager": manager,
"session": sess,
}).Trace("Account details request") }).Trace("Account details request")
if manager == nil { if manager == nil {

View File

@ -90,6 +90,8 @@ func New() *WebAPI {
api.router.GET("/project/monitoring-between/:id", LogRequestMiddleware(api.GetSnapshotsBetween)) api.router.GET("/project/monitoring-between/:id", LogRequestMiddleware(api.GetSnapshotsBetween))
api.router.GET("/project/monitoring/:id", LogRequestMiddleware(api.GetNSnapshots)) api.router.GET("/project/monitoring/:id", LogRequestMiddleware(api.GetNSnapshots))
api.router.GET("/project/assignees/:id", LogRequestMiddleware(api.ProjectGetAssigneeStats)) api.router.GET("/project/assignees/:id", LogRequestMiddleware(api.ProjectGetAssigneeStats))
api.router.GET("/project/requests/:id", LogRequestMiddleware(api.ProjectGetAccessRequests))
api.router.GET("/project/request_access/:id", LogRequestMiddleware(api.WorkerRequestAccess))
api.router.POST("/task/create", LogRequestMiddleware(api.TaskCreate)) api.router.POST("/task/create", LogRequestMiddleware(api.TaskCreate))
api.router.GET("/task/get/:project", LogRequestMiddleware(api.TaskGetFromProject)) api.router.GET("/task/get/:project", LogRequestMiddleware(api.TaskGetFromProject))

View File

@ -15,6 +15,7 @@ type CreateProjectRequest struct {
Priority int64 `json:"priority"` Priority int64 `json:"priority"`
Motd string `json:"motd"` Motd string `json:"motd"`
Public bool `json:"public"` Public bool `json:"public"`
Hidden bool `json:"hidden"`
} }
type UpdateProjectRequest struct { type UpdateProjectRequest struct {
@ -24,6 +25,7 @@ type UpdateProjectRequest struct {
Priority int64 `json:"priority"` Priority int64 `json:"priority"`
Motd string `json:"motd"` Motd string `json:"motd"`
Public bool `json:"public"` Public bool `json:"public"`
Hidden bool `json:"hidden"`
} }
type UpdateProjectResponse struct { type UpdateProjectResponse struct {
@ -55,8 +57,22 @@ type GetAssigneeStatsResponse struct {
Assignees *[]storage.AssignedTasks `json:"assignees"` Assignees *[]storage.AssignedTasks `json:"assignees"`
} }
type WorkerRequestAccessResponse struct {
Ok bool `json:"ok"`
Message string `json:"message,omitempty"`
}
type ProjectGetAccessRequestsResponse struct {
Ok bool `json:"ok"`
Message string `json:"message,omitempty"`
Requests *[]storage.Worker `json:"requests,omitempty"`
}
func (api *WebAPI) ProjectCreate(r *Request) { func (api *WebAPI) ProjectCreate(r *Request) {
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
createReq := &CreateProjectRequest{} createReq := &CreateProjectRequest{}
err := json.Unmarshal(r.Ctx.Request.Body(), createReq) err := json.Unmarshal(r.Ctx.Request.Body(), createReq)
if err != nil { if err != nil {
@ -74,26 +90,10 @@ func (api *WebAPI) ProjectCreate(r *Request) {
Priority: createReq.Priority, Priority: createReq.Priority,
Motd: createReq.Motd, Motd: createReq.Motd,
Public: createReq.Public, Public: createReq.Public,
Hidden: createReq.Hidden,
} }
if isValidProject(project) { if !isValidProject(project) {
id, err := api.Database.SaveProject(project)
if err != nil {
r.Json(CreateProjectResponse{
Ok: false,
Message: err.Error(),
}, 500)
} else {
r.OkJson(CreateProjectResponse{
Ok: true,
Id: id,
})
logrus.WithFields(logrus.Fields{
"project": project,
}).Debug("Created project")
}
} else {
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"project": project, "project": project,
}).Warn("Invalid project") }).Warn("Invalid project")
@ -102,7 +102,36 @@ func (api *WebAPI) ProjectCreate(r *Request) {
Ok: false, Ok: false,
Message: "Invalid project", Message: "Invalid project",
}, 400) }, 400)
return
} }
if !isProjectCreationAuthorized(project, manager) {
logrus.WithFields(logrus.Fields{
"project": project,
}).Warn("Unauthorized project creation")
r.Json(CreateProjectResponse{
Ok: false,
Message: "You are not permitted to create a project with this configuration",
}, 400)
return
}
id, err := api.Database.SaveProject(project)
if err != nil {
r.Json(CreateProjectResponse{
Ok: false,
Message: err.Error(),
}, 500)
return
}
r.OkJson(CreateProjectResponse{
Ok: true,
Id: id,
})
logrus.WithFields(logrus.Fields{
"project": project,
}).Debug("Created project")
} }
func (api *WebAPI) ProjectUpdate(r *Request) { func (api *WebAPI) ProjectUpdate(r *Request) {
@ -133,6 +162,7 @@ func (api *WebAPI) ProjectUpdate(r *Request) {
Priority: updateReq.Priority, Priority: updateReq.Priority,
Motd: updateReq.Motd, Motd: updateReq.Motd,
Public: updateReq.Public, Public: updateReq.Public,
Hidden: updateReq.Hidden,
} }
if isValidProject(project) { if isValidProject(project) {
@ -180,6 +210,38 @@ func isValidProject(project *storage.Project) bool {
return true return true
} }
func isProjectCreationAuthorized(project *storage.Project, manager interface{}) bool {
return true
if manager == nil {
return false
}
if project.Public && manager.(*storage.Manager).WebsiteAdmin {
return false
}
return true
}
func isProjectUpdateAuthorized(project *storage.Project, manager interface{}, db *storage.Database) bool {
var man storage.Manager
if manager != nil {
man = manager.(storage.Manager)
}
if man.WebsiteAdmin {
return true
}
role := db.ManagerHasRoleOn(&man, project.Id)
if role&storage.ROLE_EDIT == 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)
@ -230,3 +292,64 @@ func (api *WebAPI) ProjectGetAssigneeStats(r *Request) {
Assignees: stats, Assignees: stats,
}) })
} }
func (api *WebAPI) ProjectGetAccessRequests(r *Request) {
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
handleErr(err, r) //todo handle invalid id
if manager == nil {
r.Json(ProjectGetAccessRequestsResponse{
Ok: false,
Message: "Unauthorized",
}, 401)
return
}
if !manager.(*storage.Manager).WebsiteAdmin &&
api.Database.ManagerHasRoleOn(manager.(*storage.Manager), 1)&
storage.ROLE_MANAGE_ACCESS == 0 {
r.Json(ProjectGetAccessRequestsResponse{
Ok: false,
Message: "Unauthorized",
}, 403)
return
}
requests := api.Database.GetAllAccessRequests(id)
r.OkJson(ProjectGetAccessRequestsResponse{
Ok: true,
Requests: requests,
})
}
func (api *WebAPI) WorkerRequestAccess(r *Request) {
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
handleErr(err, r) //todo handle invalid id
worker, err := api.validateSignature(r)
if err != nil {
r.Json(WorkerRequestAccessResponse{
Ok: false,
Message: err.Error(),
}, 401)
}
res := api.Database.SaveAccessRequest(worker, id)
if res {
r.OkJson(WorkerRequestAccessResponse{
Ok: true,
})
} else {
r.Json(WorkerRequestAccessResponse{
Ok: false,
Message: "Project is public, you already have " +
"an active request or you already have access to this project",
}, 400)
}
}

View File

@ -177,6 +177,10 @@ func (api WebAPI) validateSignature(r *Request) (*storage.Worker, error) {
widStr := string(r.Ctx.Request.Header.Peek("X-Worker-Id")) widStr := string(r.Ctx.Request.Header.Peek("X-Worker-Id"))
signature := r.Ctx.Request.Header.Peek("X-Signature") signature := r.Ctx.Request.Header.Peek("X-Signature")
if widStr == "" {
return nil, errors.New("worker id not specified")
}
wid, err := strconv.ParseInt(widStr, 10, 64) wid, err := strconv.ParseInt(widStr, 10, 64)
if err != nil { if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{ logrus.WithError(err).WithFields(logrus.Fields{

View File

@ -58,11 +58,9 @@ func (api *WebAPI) WorkerCreate(r *Request) {
return return
} }
identity := getIdentity(r) if !canCreateWorker(r, workerReq) {
if !canCreateWorker(r, workerReq, identity) {
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"identity": identity,
"createWorkerRequest": workerReq, "createWorkerRequest": workerReq,
}).Warn("Failed CreateWorkerRequest") }).Warn("Failed CreateWorkerRequest")
@ -73,7 +71,7 @@ func (api *WebAPI) WorkerCreate(r *Request) {
return return
} }
worker, err := api.workerCreate(workerReq, getIdentity(r)) worker, err := api.workerCreate(workerReq)
if err != nil { if err != nil {
handleErr(err, r) handleErr(err, r)
} else { } else {
@ -185,7 +183,7 @@ func (api *WebAPI) WorkerUpdate(r *Request) {
r.Json(GetTaskResponse{ r.Json(GetTaskResponse{
Ok: false, Ok: false,
Message: err.Error(), Message: err.Error(),
}, 403) }, 401)
return return
} }
@ -224,7 +222,7 @@ func (api *WebAPI) GetAllWorkerStats(r *Request) {
}) })
} }
func (api *WebAPI) workerCreate(request *CreateWorkerRequest, identity *storage.Identity) (*storage.Worker, error) { func (api *WebAPI) workerCreate(request *CreateWorkerRequest) (*storage.Worker, error) {
if request.Alias == "" { if request.Alias == "" {
request.Alias = "default_alias" request.Alias = "default_alias"
@ -232,7 +230,6 @@ func (api *WebAPI) workerCreate(request *CreateWorkerRequest, identity *storage.
worker := storage.Worker{ worker := storage.Worker{
Created: time.Now().Unix(), Created: time.Now().Unix(),
Identity: identity,
Secret: makeSecret(), Secret: makeSecret(),
Alias: request.Alias, Alias: request.Alias,
} }
@ -241,7 +238,7 @@ func (api *WebAPI) workerCreate(request *CreateWorkerRequest, identity *storage.
return &worker, nil return &worker, nil
} }
func canCreateWorker(r *Request, cwr *CreateWorkerRequest, identity *storage.Identity) bool { func canCreateWorker(r *Request, cwr *CreateWorkerRequest) bool {
if cwr.Alias == "unassigned" { if cwr.Alias == "unassigned" {
//Reserved alias //Reserved alias
@ -260,13 +257,3 @@ func makeSecret() []byte {
return secret return secret
} }
func getIdentity(r *Request) *storage.Identity {
identity := storage.Identity{
RemoteAddr: r.Ctx.RemoteAddr().String(),
UserAgent: string(r.Ctx.UserAgent()),
}
return &identity
}

View File

@ -1,39 +1,30 @@
DROP TABLE IF EXISTS worker_identity, worker, project, task, log_entry, DROP TABLE IF EXISTS worker, project, task, log_entry,
worker_has_access_to_project, manager, manager_has_role_on_project, project_monitoring_snapshot, worker_has_access_to_project, manager, manager_has_role_on_project, project_monitoring_snapshot,
worker_verifies_task; worker_verifies_task, worker_requests_access_to_project;
DROP TYPE IF EXISTS status; DROP TYPE IF EXISTS status;
DROP TYPE IF EXISTS log_level; DROP TYPE IF EXISTS log_level;
CREATE TABLE worker_identity
(
id SERIAL PRIMARY KEY,
remote_addr TEXT,
user_agent TEXT,
UNIQUE (remote_addr)
);
CREATE TABLE worker CREATE TABLE worker
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY NOT NULL,
alias TEXT, alias TEXT NOT NULL,
created INTEGER, created INTEGER NOT NULL,
identity INTEGER REFERENCES worker_identity (id), secret BYTEA NOT NULL,
secret BYTEA, closed_task_count INTEGER DEFAULT 0 NOT NULL
closed_task_count INTEGER DEFAULT 0
); );
CREATE TABLE project CREATE TABLE project
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY NOT NULL,
priority INTEGER DEFAULT 0, priority INTEGER DEFAULT 0 NOT NULL,
name TEXT UNIQUE, closed_task_count INT DEFAULT 0 NOT NULL,
clone_url TEXT, public boolean NOT NULL,
git_repo TEXT UNIQUE, hidden boolean NOT NULL,
version TEXT, name TEXT UNIQUE NOT NULL,
motd TEXT, clone_url TEXT NOT NULL,
public boolean, git_repo TEXT UNIQUE NOT NULL,
closed_task_count INT DEFAULT 0 version TEXT NOT NULL,
motd TEXT NOT NULL
); );
CREATE TABLE worker_has_access_to_project CREATE TABLE worker_has_access_to_project
@ -61,43 +52,51 @@ CREATE TABLE task
CREATE TABLE worker_verifies_task CREATE TABLE worker_verifies_task
( (
verification_hash BIGINT, verification_hash BIGINT NOT NULL,
task BIGINT REFERENCES task (id) ON DELETE CASCADE, task BIGINT REFERENCES task (id) ON DELETE CASCADE NOT NULL,
worker INT REFERENCES worker (id) worker INT REFERENCES worker (id) NOT NULL
); );
CREATE TABLE log_entry CREATE TABLE log_entry
( (
level INTEGER, level INTEGER NOT NULL,
message TEXT, message TEXT NOT NULL,
message_data TEXT, message_data TEXT NOT NULL,
timestamp INTEGER timestamp INTEGER NOT NULL
); );
CREATE TABLE manager CREATE TABLE manager
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
username TEXT UNIQUE, register_time INTEGER NOT NULL,
password BYTEA, website_admin BOOLEAN NOT NULL,
website_admin BOOLEAN username TEXT UNIQUE NOT NULL,
password BYTEA NOT NULL
); );
CREATE TABLE manager_has_role_on_project CREATE TABLE manager_has_role_on_project
( (
manager INTEGER REFERENCES manager (id), manager INTEGER REFERENCES manager (id) NOT NULL,
role SMALLINT, role SMALLINT NOT NULl,
project INTEGER REFERENCES project (id) project INTEGER REFERENCES project (id) NOT NULL
); );
CREATE TABLE project_monitoring_snapshot CREATE TABLE project_monitoring_snapshot
( (
project INT REFERENCES project (id) NOT NULL,
new_task_count INT NOT NULL,
failed_task_count INT NOT NULL,
closed_task_count INT NOT NULL,
awaiting_verification_task_count INT NOT NULL,
worker_access_count INT NOT NULL,
timestamp INT NOT NULL
);
CREATE TABLE worker_requests_access_to_project
(
worker INT REFERENCES worker (id),
project INT REFERENCES project (id), project INT REFERENCES project (id),
new_task_count INT, PRIMARY KEY (worker, project)
failed_task_count INT,
closed_task_count INT,
awaiting_verification_task_count INT,
worker_access_count INT,
timestamp INT
); );
CREATE OR REPLACE FUNCTION on_task_delete_proc() RETURNS TRIGGER AS CREATE OR REPLACE FUNCTION on_task_delete_proc() RETURNS TRIGGER AS
@ -114,6 +113,21 @@ CREATE TRIGGER on_task_delete
FOR EACH ROW FOR EACH ROW
EXECUTE PROCEDURE on_task_delete_proc(); EXECUTE PROCEDURE on_task_delete_proc();
CREATE OR REPLACE FUNCTION on_manager_insert() RETURNS TRIGGER AS
$$
BEGIN
IF NEW.id = 1 THEN
UPDATE manager SET website_admin= TRUE WHERE id = 1;
end if;
RETURN NEW;
END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER on_manager_insert
AFTER INSERT
ON manager
FOR EACH ROW
EXECUTE PROCEDURE on_manager_insert();
CREATE OR REPLACE FUNCTION release_task_ok(wid INT, tid INT, ver INT) RETURNS BOOLEAN AS CREATE OR REPLACE FUNCTION release_task_ok(wid INT, tid INT, ver INT) RETURNS BOOLEAN AS
$$ $$
DECLARE DECLARE

View File

@ -10,9 +10,10 @@ import (
type ManagerRole int type ManagerRole int
const ( const (
ROLE_NONE ManagerRole = 0
ROLE_READ ManagerRole = 1 ROLE_READ ManagerRole = 1
ROLE_EDIT ManagerRole = 2 ROLE_EDIT ManagerRole = 2
ROLE_MANAGE_ACCESS ManagerRole = 3 ROLE_MANAGE_ACCESS ManagerRole = 4
) )
type Manager struct { type Manager struct {
@ -65,8 +66,8 @@ func (database *Database) SaveManager(manager *Manager, password []byte) error {
hash.Write([]byte(manager.Username)) hash.Write([]byte(manager.Username))
hashedPassword := hash.Sum(nil) hashedPassword := hash.Sum(nil)
row := db.QueryRow(`INSERT INTO manager (username, password, website_admin) row := db.QueryRow(`INSERT INTO manager (username, password, website_admin, register_time)
VALUES ($1,$2,$3) RETURNING ID`, VALUES ($1,$2,$3, extract(epoch from now() at time zone 'utc')) RETURNING ID`,
manager.Username, hashedPassword, manager.WebsiteAdmin) manager.Username, hashedPassword, manager.WebsiteAdmin)
err := row.Scan(&manager.Id) err := row.Scan(&manager.Id)
@ -78,6 +79,8 @@ func (database *Database) SaveManager(manager *Manager, password []byte) error {
return err return err
} }
manager.WebsiteAdmin = manager.Id == 1
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"manager": manager, "manager": manager,
}).Info("Database.SaveManager INSERT") }).Info("Database.SaveManager INSERT")
@ -121,3 +124,19 @@ func (database *Database) UpdateManagerPassword(manager *Manager, newPassword []
"id": manager.Id, "id": manager.Id,
}).Warning("Database.UpdateManagerPassword UPDATE") }).Warning("Database.UpdateManagerPassword UPDATE")
} }
func (database *Database) ManagerHasRoleOn(manager *Manager, projectId int64) ManagerRole {
db := database.getDB()
row := db.QueryRow(`SELECT role FROM manager_has_role_on_project
WHERE project=$1 AND manager=$2`, projectId, manager.Id)
var role ManagerRole
err := row.Scan(&role)
if err != nil {
return ROLE_NONE
}
return role
}

View File

@ -47,7 +47,7 @@ func (database *Database) MakeProjectSnapshots() {
"took": time.Now().Sub(startTime), "took": time.Now().Sub(startTime),
"add": inserted, "add": inserted,
"remove": deleted, "remove": deleted,
}).Trace("Took monitoring snapshot") }).Trace("Took project monitoring snapshot")
} }
func (database *Database) GetMonitoringSnapshotsBetween(pid int64, from int, to int) (ss *[]ProjectMonitoringSnapshot) { func (database *Database) GetMonitoringSnapshotsBetween(pid int64, from int, to int) (ss *[]ProjectMonitoringSnapshot) {

View File

@ -15,6 +15,7 @@ type Project struct {
Version string `json:"version"` Version string `json:"version"`
Motd string `json:"motd"` Motd string `json:"motd"`
Public bool `json:"public"` Public bool `json:"public"`
Hidden bool `json:"hidden"`
} }
type AssignedTasks struct { type AssignedTasks struct {
@ -31,9 +32,10 @@ func (database *Database) SaveProject(project *Project) (int64, error) {
func saveProject(project *Project, db *sql.DB) (int64, error) { func saveProject(project *Project, db *sql.DB) (int64, error) {
row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority, motd, public) row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority, motd, public, hidden)
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id`, VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id`,
project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority, project.Motd, project.Public) project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority, project.Motd,
project.Public, project.Hidden)
var id int64 var id int64
err := row.Scan(&id) err := row.Scan(&id)
@ -64,7 +66,7 @@ func (database *Database) GetProject(id int64) *Project {
func getProject(id int64, db *sql.DB) *Project { func getProject(id int64, db *sql.DB) *Project {
row := db.QueryRow(`SELECT id, priority, name, clone_url, git_repo, version, motd, public row := db.QueryRow(`SELECT id, priority, name, clone_url, git_repo, version, motd, public, hidden
FROM project WHERE id=$1`, id) FROM project WHERE id=$1`, id)
project, err := scanProject(row) project, err := scanProject(row)
@ -87,7 +89,7 @@ func scanProject(row *sql.Row) (*Project, error) {
project := &Project{} project := &Project{}
err := row.Scan(&project.Id, &project.Priority, &project.Name, &project.CloneUrl, err := row.Scan(&project.Id, &project.Priority, &project.Name, &project.CloneUrl,
&project.GitRepo, &project.Version, &project.Motd, &project.Public) &project.GitRepo, &project.Version, &project.Motd, &project.Public, &project.Hidden)
return project, err return project, err
} }
@ -95,7 +97,7 @@ func scanProject(row *sql.Row) (*Project, error) {
func (database *Database) GetProjectWithRepoName(repoName string) *Project { func (database *Database) GetProjectWithRepoName(repoName string) *Project {
db := database.getDB() db := database.getDB()
row := db.QueryRow(`SELECT id, priority, name, clone_url, git_repo, version, motd, public row := db.QueryRow(`SELECT id, priority, name, clone_url, git_repo, version, motd, public, hidden
FROM project WHERE LOWER(git_repo)=$1`, FROM project WHERE LOWER(git_repo)=$1`,
strings.ToLower(repoName)) strings.ToLower(repoName))
@ -115,8 +117,9 @@ func (database *Database) UpdateProject(project *Project) error {
db := database.getDB() db := database.getDB()
res, err := db.Exec(`UPDATE project res, err := db.Exec(`UPDATE project
SET (priority, name, clone_url, git_repo, version, motd, public) = ($1,$2,$3,$4,$5,$6,$7) WHERE id=$8`, SET (priority, name, clone_url, git_repo, version, motd, public, hidden) = ($1,$2,$3,$4,$5,$6,$7,$8) WHERE id=$9`,
project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Motd, project.Public, project.Id) project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Motd,
project.Public, project.Hidden, project.Id)
if err != nil { if err != nil {
return err return err
} }
@ -139,26 +142,24 @@ func (database Database) GetAllProjects(workerId int64) *[]Project {
var err error var err error
if workerId == 0 { if workerId == 0 {
rows, err = db.Query(`SELECT rows, err = db.Query(`SELECT
Id, priority, name, clone_url, git_repo, version, motd, public Id, priority, name, clone_url, git_repo, version, motd, public, hidden
FROM project FROM project
LEFT JOIN worker_has_access_to_project whatp ON whatp.project = id WHERE NOT hidden
WHERE public
ORDER BY name`) ORDER BY name`)
} else { } else {
rows, err = db.Query(`SELECT rows, err = db.Query(`SELECT
Id, priority, name, clone_url, git_repo, version, motd, public Id, priority, name, clone_url, git_repo, version, motd, public, hidden
FROM project FROM project
LEFT JOIN worker_has_access_to_project whatp ON whatp.project = id LEFT JOIN worker_has_access_to_project whatp ON whatp.project = id
WHERE public OR whatp.worker = $1 WHERE NOT hidden OR whatp.worker = $1
ORDER BY name`, workerId) ORDER BY name`, workerId)
} }
handleErr(err) handleErr(err)
for rows.Next() { for rows.Next() {
p := Project{} p := Project{}
err := rows.Scan(&p.Id, &p.Priority, &p.Name, &p.CloneUrl, err := rows.Scan(&p.Id, &p.Priority, &p.Name, &p.CloneUrl,
&p.GitRepo, &p.Version, &p.Motd, &p.Public) &p.GitRepo, &p.Version, &p.Motd, &p.Public, &p.Hidden)
handleErr(err) handleErr(err)
projects = append(projects, p) projects = append(projects, p)
} }

View File

@ -1,20 +1,12 @@
package storage package storage
import ( import (
"database/sql"
"errors"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
) )
type Identity struct {
RemoteAddr string `json:"remote_addr"`
UserAgent string `json:"user_agent"`
}
type Worker struct { type Worker struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Created int64 `json:"created"` Created int64 `json:"created"`
Identity *Identity `json:"identity"`
Alias string `json:"alias,omitempty"` Alias string `json:"alias,omitempty"`
Secret []byte `json:"secret"` Secret []byte `json:"secret"`
} }
@ -28,10 +20,8 @@ func (database *Database) SaveWorker(worker *Worker) {
db := database.getDB() db := database.getDB()
identityId := getOrCreateIdentity(worker.Identity, db) row := db.QueryRow("INSERT INTO worker (created, secret, alias) VALUES ($1,$2,$3) RETURNING id",
worker.Created, worker.Secret, worker.Alias)
row := db.QueryRow("INSERT INTO worker (created, identity, secret, alias) VALUES ($1,$2,$3,$4) RETURNING id",
worker.Created, identityId, worker.Secret, worker.Alias)
err := row.Scan(&worker.Id) err := row.Scan(&worker.Id)
handleErr(err) handleErr(err)
@ -46,10 +36,9 @@ func (database *Database) GetWorker(id int64) *Worker {
db := database.getDB() db := database.getDB()
worker := &Worker{} worker := &Worker{}
var identityId int64
row := db.QueryRow("SELECT id, created, identity, secret, alias FROM worker WHERE id=$1", id) row := db.QueryRow("SELECT id, created, secret, alias FROM worker WHERE id=$1", id)
err := row.Scan(&worker.Id, &worker.Created, &identityId, &worker.Secret, &worker.Alias) err := row.Scan(&worker.Id, &worker.Created, &worker.Secret, &worker.Alias)
if err != nil { if err != nil {
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"id": id, "id": id,
@ -57,9 +46,6 @@ func (database *Database) GetWorker(id int64) *Worker {
return nil return nil
} }
worker.Identity, err = getIdentity(identityId, db)
handleErr(err)
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"worker": worker, "worker": worker,
}).Trace("Database.getWorker SELECT worker") }).Trace("Database.getWorker SELECT worker")
@ -67,48 +53,6 @@ func (database *Database) GetWorker(id int64) *Worker {
return worker return worker
} }
func getIdentity(id int64, db *sql.DB) (*Identity, error) {
identity := &Identity{}
row := db.QueryRow("SELECT remote_addr, user_agent FROM worker_identity WHERE id=$1", id)
err := row.Scan(&identity.RemoteAddr, &identity.UserAgent)
if err != nil {
return nil, errors.New("identity not found")
}
logrus.WithFields(logrus.Fields{
"identity": identity,
}).Trace("Database.getIdentity SELECT workerIdentity")
return identity, nil
}
func getOrCreateIdentity(identity *Identity, db *sql.DB) int64 {
res, err := db.Exec("INSERT INTO worker_identity (remote_addr, user_agent) VALUES ($1,$2) ON CONFLICT DO NOTHING",
identity.RemoteAddr, identity.UserAgent)
handleErr(err)
rowsAffected, err := res.RowsAffected()
logrus.WithFields(logrus.Fields{
"rowsAffected": rowsAffected,
}).Trace("Database.saveWorker INSERT workerIdentity")
row := db.QueryRow("SELECT (id) FROM worker_identity WHERE remote_addr=$1", identity.RemoteAddr)
var rowId int64
err = row.Scan(&rowId)
handleErr(err)
logrus.WithFields(logrus.Fields{
"rowId": rowId,
}).Trace("Database.saveWorker SELECT workerIdentity")
return rowId
}
func (database *Database) GrantAccess(workerId int64, projectId int64) bool { func (database *Database) GrantAccess(workerId int64, projectId int64) bool {
db := database.getDB() db := database.getDB()
@ -169,6 +113,88 @@ func (database *Database) UpdateWorker(worker *Worker) bool {
return rowsAffected == 1 return rowsAffected == 1
} }
func (database *Database) SaveAccessRequest(worker *Worker, projectId int64) bool {
db := database.getDB()
res, err := db.Exec(`INSERT INTO worker_requests_access_to_project
SELECT $1, id FROM project WHERE id=$2 AND NOT project.public
AND NOT EXISTS(SELECT * FROM worker_has_access_to_project WHERE worker=$1 AND project=$2)`,
worker.Id, projectId)
if err != nil {
return false
}
rowsAffected, _ := res.RowsAffected()
logrus.WithFields(logrus.Fields{
"rowsAffected": rowsAffected,
}).Trace("Database.SaveAccessRequest INSERT")
return rowsAffected == 1
}
func (database *Database) AcceptAccessRequest(worker *Worker, projectId int64) bool {
db := database.getDB()
res, err := db.Exec(`DELETE FROM worker_requests_access_to_project
WHERE worker=$1 AND project=$2`)
handleErr(err)
rowsAffected, _ := res.RowsAffected()
if rowsAffected == 1 {
_, err := db.Exec(`INSERT INTO worker_has_access_to_project
(worker, project) VALUES ($1,$2)`,
worker.Id, projectId)
handleErr(err)
}
logrus.WithFields(logrus.Fields{
"rowsAffected": rowsAffected,
}).Trace("Database.AcceptAccessRequest")
return rowsAffected == 1
}
func (database *Database) RejectAccessRequest(worker *Worker, projectId int64) bool {
db := database.getDB()
res, err := db.Exec(`DELETE FROM worker_requests_access_to_project
WHERE worker=$1 AND project=$2`, worker.Id, projectId)
handleErr(err)
rowsAffected, _ := res.RowsAffected()
logrus.WithFields(logrus.Fields{
"rowsAffected": rowsAffected,
}).Trace("Database.AcceptAccessRequest")
return rowsAffected == 1
}
func (database *Database) GetAllAccessRequests(projectId int64) *[]Worker {
db := database.getDB()
rows, err := db.Query(`SELECT id, alias, created FROM worker_requests_access_to_project
INNER JOIN worker w on worker_requests_access_to_project.worker = w.id
WHERE project=$1`,
projectId)
handleErr(err)
requests := make([]Worker, 0)
for rows.Next() {
w := Worker{}
_ = rows.Scan(&w.Id, &w.Alias, &w.Created)
requests = append(requests, w)
}
return &requests
}
func (database *Database) GetAllWorkerStats() *[]WorkerStats { func (database *Database) GetAllWorkerStats() *[]WorkerStats {
db := database.getDB() db := database.getDB()

View File

@ -163,3 +163,7 @@ func login(request *api.LoginRequest) (*api.LoginResponse, *http.Response) {
return resp, r return resp, r
} }
func getSessionCtx(username string, password string, admin bool) {
}

View File

@ -19,6 +19,7 @@ func TestCreateGetProject(t *testing.T) {
Priority: 123, Priority: 123,
Motd: "motd", Motd: "motd",
Public: true, Public: true,
Hidden: true,
}) })
id := resp.Id id := resp.Id
@ -59,6 +60,9 @@ func TestCreateGetProject(t *testing.T) {
if getResp.Project.Public != true { if getResp.Project.Public != true {
t.Error() t.Error()
} }
if getResp.Project.Hidden != true {
t.Error()
}
} }
func TestCreateProjectInvalid(t *testing.T) { func TestCreateProjectInvalid(t *testing.T) {
@ -141,6 +145,7 @@ func TestUpdateProjectValid(t *testing.T) {
Name: "NameB", Name: "NameB",
Motd: "MotdB", Motd: "MotdB",
Public: false, Public: false,
Hidden: true,
}, pid) }, pid)
if updateResp.Ok != true { if updateResp.Ok != true {
@ -164,6 +169,9 @@ func TestUpdateProjectValid(t *testing.T) {
if proj.Project.Priority != 2 { if proj.Project.Priority != 2 {
t.Error() t.Error()
} }
if proj.Project.Hidden != true {
t.Error()
}
} }
func TestUpdateProjectInvalid(t *testing.T) { func TestUpdateProjectInvalid(t *testing.T) {

View File

@ -33,12 +33,6 @@ func TestCreateGetWorker(t *testing.T) {
t.Error() t.Error()
} }
if len(getResp.Worker.Identity.RemoteAddr) <= 0 {
t.Error()
}
if len(getResp.Worker.Identity.UserAgent) <= 0 {
t.Error()
}
if resp.Worker.Alias != "my_worker_alias" { if resp.Worker.Alias != "my_worker_alias" {
t.Error() t.Error()
} }

View File

@ -15,6 +15,11 @@ import (
"strconv" "strconv"
) )
type SessionContext struct {
Manager *storage.Manager
SessionCookie *http.Cookie
}
func Post(path string, x interface{}, worker *storage.Worker) *http.Response { func Post(path string, x interface{}, worker *storage.Worker) *http.Response {
body, err := json.Marshal(x) body, err := json.Marshal(x)

View File

@ -1,39 +1,30 @@
DROP TABLE IF EXISTS worker_identity, worker, project, task, log_entry, DROP TABLE IF EXISTS worker, project, task, log_entry,
worker_has_access_to_project, manager, manager_has_role_on_project, project_monitoring_snapshot, worker_has_access_to_project, manager, manager_has_role_on_project, project_monitoring_snapshot,
worker_verifies_task; worker_verifies_task, worker_requests_access_to_project;
DROP TYPE IF EXISTS status; DROP TYPE IF EXISTS status;
DROP TYPE IF EXISTS log_level; DROP TYPE IF EXISTS log_level;
CREATE TABLE worker_identity
(
id SERIAL PRIMARY KEY,
remote_addr TEXT,
user_agent TEXT,
UNIQUE (remote_addr)
);
CREATE TABLE worker CREATE TABLE worker
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY NOT NULL,
alias TEXT, alias TEXT NOT NULL,
created INTEGER, created INTEGER NOT NULL,
identity INTEGER REFERENCES worker_identity (id), secret BYTEA NOT NULL,
secret BYTEA, closed_task_count INTEGER DEFAULT 0 NOT NULL
closed_task_count INTEGER DEFAULT 0
); );
CREATE TABLE project CREATE TABLE project
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY NOT NULL,
priority INTEGER DEFAULT 0, priority INTEGER DEFAULT 0 NOT NULL,
name TEXT UNIQUE, closed_task_count INT DEFAULT 0 NOT NULL,
clone_url TEXT, public boolean NOT NULL,
git_repo TEXT UNIQUE, hidden boolean NOT NULL,
version TEXT, name TEXT UNIQUE NOT NULL,
motd TEXT, clone_url TEXT NOT NULL,
public boolean, git_repo TEXT UNIQUE NOT NULL,
closed_task_count INT DEFAULT 0 version TEXT NOT NULL,
motd TEXT NOT NULL
); );
CREATE TABLE worker_has_access_to_project CREATE TABLE worker_has_access_to_project
@ -61,43 +52,50 @@ CREATE TABLE task
CREATE TABLE worker_verifies_task CREATE TABLE worker_verifies_task
( (
verification_hash BIGINT, verification_hash BIGINT NOT NULL,
task BIGINT REFERENCES task (id) ON DELETE CASCADE, task BIGINT REFERENCES task (id) ON DELETE CASCADE NOT NULL,
worker INT REFERENCES worker (id) worker INT REFERENCES worker (id) NOT NULL
); );
CREATE TABLE log_entry CREATE TABLE log_entry
( (
level INTEGER, level INTEGER NOT NULL,
message TEXT, message TEXT NOT NULL,
message_data TEXT, message_data TEXT NOT NULL,
timestamp INTEGER timestamp INTEGER NOT NULL
); );
CREATE TABLE manager CREATE TABLE manager
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
username TEXT UNIQUE, register_time INTEGER NOT NULL,
password BYTEA, website_admin BOOLEAN NOT NULL,
website_admin BOOLEAN username TEXT UNIQUE NOT NULL,
password BYTEA NOT NULL
); );
CREATE TABLE manager_has_role_on_project CREATE TABLE manager_has_role_on_project
( (
manager INTEGER REFERENCES manager (id), manager INTEGER REFERENCES manager (id) NOT NULL,
role SMALLINT, role SMALLINT NOT NULl,
project INTEGER REFERENCES project (id) project INTEGER REFERENCES project (id) NOT NULL
); );
CREATE TABLE project_monitoring_snapshot CREATE TABLE project_monitoring_snapshot
( (
project INT REFERENCES project (id), project INT REFERENCES project (id) NOT NULL,
new_task_count INT, new_task_count INT NOT NULL,
failed_task_count INT, failed_task_count INT NOT NULL,
closed_task_count INT, closed_task_count INT NOT NULL,
awaiting_verification_task_count INT, awaiting_verification_task_count INT NOT NULL,
worker_access_count INT, worker_access_count INT NOT NULL,
timestamp INT timestamp INT NOT NULL
);
CREATE TABLE worker_requests_access_to_project
(
worker INT REFERENCES worker (id) NOT NULL,
project INT REFERENCES project (id) NOT NULL
); );
CREATE OR REPLACE FUNCTION on_task_delete_proc() RETURNS TRIGGER AS CREATE OR REPLACE FUNCTION on_task_delete_proc() RETURNS TRIGGER AS
@ -114,6 +112,21 @@ CREATE TRIGGER on_task_delete
FOR EACH ROW FOR EACH ROW
EXECUTE PROCEDURE on_task_delete_proc(); EXECUTE PROCEDURE on_task_delete_proc();
CREATE OR REPLACE FUNCTION on_manager_insert() RETURNS TRIGGER AS
$$
BEGIN
IF NEW.id = 1 THEN
UPDATE manager SET website_admin= TRUE WHERE id = 1;
end if;
RETURN NEW;
END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER on_manager_insert
AFTER INSERT
ON manager
FOR EACH ROW
EXECUTE PROCEDURE on_manager_insert();
CREATE OR REPLACE FUNCTION release_task_ok(wid INT, tid INT, ver INT) RETURNS BOOLEAN AS CREATE OR REPLACE FUNCTION release_task_ok(wid INT, tid INT, ver INT) RETURNS BOOLEAN AS
$$ $$
DECLARE DECLARE

View File

@ -65,4 +65,8 @@ export class ApiService {
return this.http.get(this.url + `/worker/stats`, this.options) return this.http.get(this.url + `/worker/stats`, this.options)
} }
getProjectAccessRequests(project: number) {
return this.http.get(this.url + `/project/requests/${project}`)
}
} }

View File

@ -11,6 +11,7 @@ import {TranslateService} from "@ngx-translate/core";
import {LoginComponent} from "./login/login.component"; import {LoginComponent} from "./login/login.component";
import {AccountDetailsComponent} from "./account-details/account-details.component"; import {AccountDetailsComponent} from "./account-details/account-details.component";
import {WorkerDashboardComponent} from "./worker-dashboard/worker-dashboard.component"; import {WorkerDashboardComponent} from "./worker-dashboard/worker-dashboard.component";
import {ProjectPermsComponent} from "./project-perms/project-perms.component";
const routes: Routes = [ const routes: Routes = [
{path: "log", component: LogsComponent}, {path: "log", component: LogsComponent},
@ -19,6 +20,7 @@ const routes: Routes = [
{path: "projects", component: ProjectListComponent}, {path: "projects", component: ProjectListComponent},
{path: "project/:id", component: ProjectDashboardComponent}, {path: "project/:id", component: ProjectDashboardComponent},
{path: "project/:id/update", component: UpdateProjectComponent}, {path: "project/:id/update", component: UpdateProjectComponent},
{path: "project/:id/perms", component: ProjectPermsComponent},
{path: "new_project", component: CreateProjectComponent}, {path: "new_project", component: CreateProjectComponent},
{path: "workers", component: WorkerDashboardComponent} {path: "workers", component: WorkerDashboardComponent}
]; ];

View File

@ -6,10 +6,11 @@
[routerLink]="'log'">{{"nav.logs" | translate}}</button> [routerLink]="'log'">{{"nav.logs" | translate}}</button>
<button mat-button [class.mat-accent]="router.url == '/projects'" class="nav-link" <button mat-button [class.mat-accent]="router.url == '/projects'" class="nav-link"
[routerLink]="'projects'">{{"nav.project_list" | translate}}</button> [routerLink]="'projects'">{{"nav.project_list" | translate}}</button>
<button mat-button [class.mat-accent]="router.url == '/new_project'" class="nav-link"
[routerLink]="'new_project'">{{"nav.new_project" | translate}}</button>
<button mat-button [class.mat-accent]="router.url == '/workers'" class="nav-link" <button mat-button [class.mat-accent]="router.url == '/workers'" class="nav-link"
[routerLink]="'workers'">{{"nav.worker_dashboard" | translate}}</button> [routerLink]="'workers'">{{"nav.worker_dashboard" | translate}}</button>
<button mat-button [class.mat-accent]="router.url == '/new_project'" class="nav-link"
[routerLink]="'new_project'"
*ngIf="authService.logged">{{"nav.new_project" | translate}}</button>
</div> </div>
<div class="small-nav"> <div class="small-nav">
<button mat-button [matMenuTriggerFor]="smallNav"> <button mat-button [matMenuTriggerFor]="smallNav">
@ -22,10 +23,11 @@
[routerLink]="'log'">{{"nav.logs" | translate}}</button> [routerLink]="'log'">{{"nav.logs" | translate}}</button>
<button mat-menu-item [class.mat-accent]="router.url == '/projects'" class="nav-link" <button mat-menu-item [class.mat-accent]="router.url == '/projects'" class="nav-link"
[routerLink]="'projects'">{{"nav.project_list" | translate}}</button> [routerLink]="'projects'">{{"nav.project_list" | translate}}</button>
<button mat-menu-item [class.mat-accent]="router.url == '/new_project'" class="nav-link"
[routerLink]="'new_project'">{{"nav.new_project" | translate}}</button>
<button mat-menu-item [class.mat-accent]="router.url == '/workers'" class="nav-link" <button mat-menu-item [class.mat-accent]="router.url == '/workers'" class="nav-link"
[routerLink]="'workers'">{{"nav.worker_dashboard" | translate}}</button> [routerLink]="'workers'">{{"nav.worker_dashboard" | translate}}</button>
<button mat-menu-item [class.mat-accent]="router.url == '/new_project'" class="nav-link"
[routerLink]="'new_project'"
*ngIf="authService.logged">{{"nav.new_project" | translate}}</button>
</mat-menu> </mat-menu>
</div> </div>
<span class="nav-spacer"></span> <span class="nav-spacer"></span>

View File

@ -20,7 +20,7 @@ export class AppComponent {
]; ];
constructor(private translate: TranslateService, constructor(private translate: TranslateService,
private router: Router, public router: Router,
public authService: AuthService) { public authService: AuthService) {
translate.addLangs([ translate.addLangs([

View File

@ -47,6 +47,7 @@ import {TranslatedPaginator} from "./TranslatedPaginatorConfiguration";
import {LoginComponent} from './login/login.component'; import {LoginComponent} from './login/login.component';
import {AccountDetailsComponent} from './account-details/account-details.component'; import {AccountDetailsComponent} from './account-details/account-details.component';
import {WorkerDashboardComponent} from './worker-dashboard/worker-dashboard.component'; import {WorkerDashboardComponent} from './worker-dashboard/worker-dashboard.component';
import {ProjectPermsComponent} from './project-perms/project-perms.component';
export function createTranslateLoader(http: HttpClient) { export function createTranslateLoader(http: HttpClient) {
@ -66,6 +67,7 @@ export function createTranslateLoader(http: HttpClient) {
LoginComponent, LoginComponent,
AccountDetailsComponent, AccountDetailsComponent,
WorkerDashboardComponent, WorkerDashboardComponent,
ProjectPermsComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -10,6 +10,7 @@ import {Router} from "@angular/router";
export class AuthService { export class AuthService {
account: Manager; account: Manager;
logged: boolean;
constructor(private apiService: ApiService, constructor(private apiService: ApiService,
private messengerService: MessengerService, private messengerService: MessengerService,
@ -17,6 +18,7 @@ export class AuthService {
this.apiService.getAccountDetails() this.apiService.getAccountDetails()
.subscribe((data: any) => { .subscribe((data: any) => {
this.account = data.manager; this.account = data.manager;
this.logged = data.logged_in;
}) })
} }
@ -27,6 +29,7 @@ export class AuthService {
this.apiService.getAccountDetails() this.apiService.getAccountDetails()
.subscribe((data: any) => { .subscribe((data: any) => {
this.account = data.manager; this.account = data.manager;
this.logged = true;
this.router.navigateByUrl("/account"); this.router.navigateByUrl("/account");
}) })
}, },
@ -42,7 +45,8 @@ export class AuthService {
.subscribe( .subscribe(
() => { () => {
this.account = null; this.account = null;
this.router.navigateByUrl(""); this.logged = false;
this.router.navigateByUrl("login");
}, },
error => { error => {
console.log(error); console.log(error);

View File

@ -3,40 +3,47 @@
<mat-tab-group> <mat-tab-group>
<mat-tab [label]="'login.title' | translate" class="pad"> <mat-tab [label]="'login.title' | translate" class="pad">
<form (ngSubmit)="login()">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>{{"login.username" | translate}}</mat-label> <mat-label>{{"login.username" | translate}}</mat-label>
<input type="text" matInput [(ngModel)]="credentials.username"> <input type="text" matInput [(ngModel)]="credentials.username"
[ngModelOptions]="{standalone: true}">
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>{{ "login.password" | translate}}</mat-label> <mat-label>{{ "login.password" | translate}}</mat-label>
<input type="password" matInput [(ngModel)]="credentials.password"> <input type="password" matInput [(ngModel)]="credentials.password"
[ngModelOptions]="{standalone: true}">
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
(click)="login()">{{"login.login" | translate}}</button> type="submit">{{"login.login" | translate}}</button>
</form>
</mat-tab> </mat-tab>
<mat-tab [label]="'create_account.title' | translate" class="pad"> <mat-tab [label]="'create_account.title' | translate" class="pad">
<form (ngSubmit)="register()">
<mat-form-field appearance="outline" [hideRequiredMarker]="true"> <mat-form-field appearance="outline" [hideRequiredMarker]="true">
<mat-label>{{"login.username" | translate}}</mat-label> <mat-label>{{"login.username" | translate}}</mat-label>
<mat-hint align="end">{{credentials.username?.length || 0}}/16</mat-hint> <mat-hint align="end">{{credentials.username?.length || 0}}/16</mat-hint>
<input maxlength="16" type="text" matInput [(ngModel)]="credentials.username" name="username" <input maxlength="16" type="text" matInput [(ngModel)]="credentials.username" name="username"
required> [ngModelOptions]="{standalone: true}" required>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" [hideRequiredMarker]="true"> <mat-form-field appearance="outline" [hideRequiredMarker]="true">
<mat-label>{{ "login.password" | translate}}</mat-label> <mat-label>{{ "login.password" | translate}}</mat-label>
<input type="password" matInput [(ngModel)]="credentials.password" name="password" required> <input type="password" matInput [(ngModel)]="credentials.password" name="password"
[ngModelOptions]="{standalone: true}">
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>{{ "login.repeat_password" | translate}}</mat-label> <mat-label>{{ "login.repeat_password" | translate}}</mat-label>
<input type="password" matInput [(ngModel)]="credentials.repeatPassword" name="password2" <input type="password" matInput [(ngModel)]="credentials.repeatPassword" name="password2"
> [ngModelOptions]="{standalone: true}" required>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" [disabled]="!canCreate()" <button mat-raised-button color="primary" [disabled]="!canCreate()"
(click)="register()">{{"create_account.create" | translate}}</button> type="submit">{{"create_account.create" | translate}}</button>
</form>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</mat-card> </mat-card>

View File

@ -3,6 +3,7 @@ import {MessengerService} from "../messenger.service";
import {MessengerState} from "./messenger"; import {MessengerState} from "./messenger";
import {Subscription} from "rxjs"; import {Subscription} from "rxjs";
import {MatSnackBar, MatSnackBarConfig} from "@angular/material"; import {MatSnackBar, MatSnackBarConfig} from "@angular/material";
import {TranslateService} from "@ngx-translate/core";
@Component({ @Component({
selector: 'messenger-snack-bar', selector: 'messenger-snack-bar',
@ -13,7 +14,10 @@ export class SnackBarComponent implements OnInit {
private subscription: Subscription; private subscription: Subscription;
constructor(private messengerService: MessengerService, private snackBar: MatSnackBar) { constructor(
private messengerService: MessengerService,
private snackBar: MatSnackBar,
private translate: TranslateService) {
} }
@ -23,9 +27,11 @@ export class SnackBarComponent implements OnInit {
if (state.hidden) { if (state.hidden) {
this.snackBar.dismiss(); this.snackBar.dismiss();
} else { } else {
this.snackBar.open(state.message, "Close", <MatSnackBarConfig>{ this.translate.get("messenger.close")
.subscribe(t =>
this.snackBar.open(state.message, t, <MatSnackBarConfig>{
duration: 10 * 1000, duration: 10 * 1000,
}) }))
} }
}); });
} }

View File

@ -1,3 +1,5 @@
interface Manager { interface Manager {
id: number;
username: string username: string
website_admin: boolean;
} }

View File

@ -0,0 +1,6 @@
export interface Worker {
id: number
alias: string
created: number
secret: string
}

View File

@ -56,6 +56,8 @@
<mat-card-actions> <mat-card-actions>
<button mat-raised-button color="primary" *ngIf="project" <button mat-raised-button color="primary" *ngIf="project"
[routerLink]="'/project/' + project.id + '/update'">{{"project.update" | translate}}</button> [routerLink]="'/project/' + project.id + '/update'">{{"project.update" | translate}}</button>
<button mat-raised-button color="primary" *ngIf="project"
[routerLink]="'/project/' + project.id + '/perms'">{{"project.perms" | translate}}</button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</div> </div>

View File

@ -1,3 +1,7 @@
button { button {
margin-right: 15px; margin-right: 15px;
} }
mat-panel-title > mat-icon {
margin-right: 1em;
}

View File

@ -7,16 +7,22 @@
<mat-accordion> <mat-accordion>
<mat-expansion-panel *ngFor="let project of projects"> <mat-expansion-panel *ngFor="let project of projects">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title><span style="width: 3em">{{project.id}}</span>{{project.name}} <mat-panel-title>
<mat-icon *ngIf="project.public">public</mat-icon>
<mat-icon *ngIf="!project.public">lock</mat-icon>
<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>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<pre>{{project | json}}</pre> <pre>{{project | json}}</pre>
<div> <div>
<button mat-raised-button [routerLink]="'/project/' + project.id"> <button mat-raised-button color="primary" [routerLink]="'/project/' + project.id">
<mat-icon>timeline</mat-icon>{{"projects.dashboard" | translate}}</button> <mat-icon>timeline</mat-icon>{{"projects.dashboard" | translate}}</button>
<button mat-raised-button [routerLink]="'/project/' + project.id + '/update'"> <button mat-raised-button color="primary" [routerLink]="'/project/' + project.id + '/update'">
<mat-icon>build</mat-icon>{{"project.update" | translate}}</button> <mat-icon>build</mat-icon>{{"project.update" | translate}}</button>
<button mat-raised-button color="primary" [routerLink]="'/project/' + project.id + '/perms'">
<mat-icon>perm_identity</mat-icon>
{{"project.perms" | translate}}</button>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
<span *ngIf="projects && projects.length == 0"> <span *ngIf="projects && projects.length == 0">

View File

@ -0,0 +1,15 @@
.unauthorized {
text-align: center;
color: #616161;
min-height: 3em;
margin-top: 2em !important;
}
.text-mono {
font-family: Hack, Courier, "Courier New", monospace;
color: #ff4081;
}
button {
margin-left: 15px;
}

View File

@ -0,0 +1,37 @@
<div class="container">
<mat-card class="mat-elevation-z8">
<button mat-button [title]="'perms.refresh' | translate" style="float:right"
(click)="refresh()">
<mat-icon>refresh</mat-icon>
</button>
<mat-card-header>
<mat-card-title>{{"perms.title" | translate}}</mat-card-title>
<mat-card-subtitle>{{"perms.subtitle" | translate}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<mat-list *ngIf="!unauthorized">
<mat-list-item *ngFor="let w of requests">
<mat-icon mat-list-icon>person_add</mat-icon>
<h4 mat-line>{{w.alias}}</h4>
<div mat-line>
Id=<span class="text-mono">{{w.id}}</span>, {{"perms.created" | translate}}
<span class="text-mono">{{moment.unix(w.created).format("YYYY-MM-DD HH:mm:ss UTC")}}</span>
</div>
<span style="flex: 1 1 auto;"></span>
<button mat-raised-button color="primary" [title]="'perms.grant' | translate">
<mat-icon>check</mat-icon>
</button>
<button mat-raised-button color="warn" [title]="'perms.reject' | translate">
<mat-icon>close</mat-icon>
</button>
</mat-list-item>
</mat-list>
<p *ngIf="unauthorized" class="unauthorized">
<mat-icon>block</mat-icon>
{{"perms.unauthorized" | translate}}
</p>
</mat-card-content>
</mat-card>
</div>

View File

@ -0,0 +1,60 @@
import {Worker} from "../models/worker"
import {Component, OnInit} from '@angular/core';
import {ApiService} from "../api.service";
import {Project} from "../models/project";
import {ActivatedRoute, Router} from "@angular/router";
import {MessengerService} from "../messenger.service";
import {TranslateService} from "@ngx-translate/core";
import * as moment from "moment"
@Component({
selector: 'app-project-perms',
templateUrl: './project-perms.component.html',
styleUrls: ['./project-perms.component.css']
})
export class ProjectPermsComponent implements OnInit {
constructor(private apiService: ApiService,
private route: ActivatedRoute,
private messengerService: MessengerService,
private translate: TranslateService,
private router: Router) {
}
project: Project;
private projectId: number;
requests: Worker[];
unauthorized: boolean = false;
moment = moment;
ngOnInit() {
this.route.params.subscribe(params => {
this.projectId = params["id"];
this.getProject();
this.getProjectRequests();
})
}
private getProject() {
this.apiService.getProject(this.projectId).subscribe(data => {
this.project = data["project"]
})
}
private getProjectRequests() {
this.apiService.getProjectAccessRequests(this.projectId).subscribe(
data => {
this.requests = data["requests"]
},
error => {
if (error && (error.status == 401 || error.status == 403)) {
this.unauthorized = true;
}
})
}
private refresh() {
this.getProjectRequests()
}
}

View File

@ -29,16 +29,7 @@ export class UpdateProjectComponent implements OnInit {
private getProject() { private getProject() {
this.apiService.getProject(this.projectId).subscribe(data => { this.apiService.getProject(this.projectId).subscribe(data => {
this.project = <Project>{ this.project = data["project"]
id: data["project"]["id"],
name: data["project"]["name"],
clone_url: data["project"]["clone_url"],
git_repo: data["project"]["git_repo"],
motd: data["project"]["motd"],
priority: data["project"]["priority"],
version: data["project"]["version"],
public: data["project"]["public"],
}
}) })
} }

View File

@ -57,7 +57,8 @@
"create": "Create", "create": "Create",
"git_repo": "Git repository name", "git_repo": "Git repository name",
"motd": "Message of the day", "motd": "Message of the day",
"update": "Edit" "update": "Edit",
"perms": "Permissions"
}, },
"dashboard": { "dashboard": {
"title": "Dashboard for", "title": "Dashboard for",
@ -86,5 +87,18 @@
"workers": { "workers": {
"title": "Completed tasks per worker", "title": "Completed tasks per worker",
"subtitle": "Real-time data for all projects" "subtitle": "Real-time data for all projects"
},
"perms": {
"title": "Project permissions",
"subtitle": "Workers need to request access to work on private projects",
"unauthorized": "You need ROLE_MANAGE_ACCESS on this project to access this page",
"created": "Created on",
"grant": "Accept request",
"reject": "Deny request",
"refresh": "Refresh"
},
"messenger": {
"close": "Close",
"unauthorized": "Unauthorized"
} }
} }

View File

@ -58,7 +58,8 @@
"public": "Publique", "public": "Publique",
"create": "Créer", "create": "Créer",
"motd": "Message du jour", "motd": "Message du jour",
"update": "Mettre à jour" "update": "Mettre à jour",
"perms": "Permissions"
}, },
"dashboard": { "dashboard": {
"title": "Tableau de bord pour ", "title": "Tableau de bord pour ",
@ -88,6 +89,19 @@
"workers": { "workers": {
"title": "Tâches complétés par worker", "title": "Tâches complétés par worker",
"subtitle": "Données en temps réél pour tous les projets" "subtitle": "Données en temps réél pour tous les projets"
},
"perms": {
"title": "Permissions du projet",
"subtitle": "Les Workers doivent faire un requête d'acces pour pouvoir travailler sur les projets privés",
"unauthorized": "Vous devez avoir ROLE_GESTION_ACCES sur ce project pour accéder à cette page",
"created": "Créé le",
"grant": "Accepter la requête",
"reject": "Rejeter la requête",
"refresh": "Refraichir"
},
"messenger": {
"close": "Fermer",
"unauthorized": "Non autorisé"
} }
} }