From bab0a5579c7e32b5f557faca8b1e8cea46dc8a0c Mon Sep 17 00:00:00 2001 From: Paco Nelos Date: Mon, 4 Dec 2023 21:05:17 +0000 Subject: [PATCH] Subjects initial code --- api/main.go | 6 +++++ api/principals.go | 7 ----- api/subjects.go | 43 ++++++++++++++++++++++++++++++ simulator/client.py | 17 ++++++++++++ vault/sql.go | 31 +++++++++++++++++++++ vault/vault.go | 65 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 api/subjects.go diff --git a/api/main.go b/api/main.go index 73cfb57..fb76f53 100644 --- a/api/main.go +++ b/api/main.go @@ -167,6 +167,12 @@ func SetupApi(core *Core) *fiber.App { tokensGroup.Get(":tokenId", core.GetTokenById) tokensGroup.Post("", core.CreateToken) + subjectsGroup := app.Group("/subjects") + subjectsGroup.Use(authGuard(core)) + subjectsGroup.Get(":subjectId", core.GetSubject) + subjectsGroup.Post("", core.CreateSubject) + subjectsGroup.Delete(":subjectId", core.DeleteSubject) + app.Use(func(c *fiber.Ctx) error { return c.SendStatus(404) }) diff --git a/api/principals.go b/api/principals.go index 1387efd..959800e 100644 --- a/api/principals.go +++ b/api/principals.go @@ -7,13 +7,6 @@ import ( _vault "github.com/subrose/vault" ) -// type NewPrincipal struct { -// Username string `json:"username" validate:"required,min=1,max=32"` -// Password string `json:"password" validate:"required,min=4,max=32"` // This is to limit the size of the password hash. -// Description string `json:"description"` -// Policies []string `json:"policies"` -// } - type PrincipalResponse struct { Id string `json:"id"` Username string `json:"username" validate:"required,min=3,max=32"` diff --git a/api/subjects.go b/api/subjects.go new file mode 100644 index 0000000..ecbaa75 --- /dev/null +++ b/api/subjects.go @@ -0,0 +1,43 @@ +package main + +import ( + "net/http" + + "github.com/gofiber/fiber/v2" + _vault "github.com/subrose/vault" +) + +func (core *Core) CreateSubject(c *fiber.Ctx) error { + var subject *_vault.Subject + if err := core.ParseJsonBody(c.Body(), subject); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(err) + } + + sessionPrincipal := GetSessionPrincipal(c) + err := core.vault.CreateSubject(c.Context(), sessionPrincipal, subject) + if err != nil { + return err + } + return c.Status(http.StatusCreated).JSON(subject) +} + +func (core *Core) DeleteSubject(c *fiber.Ctx) error { + sid := c.Params("subjectId") + sessionPrincipal := GetSessionPrincipal(c) + err := core.vault.DeleteSubject(c.Context(), sessionPrincipal, sid) + if err != nil { + return err + } + return c.SendStatus(http.StatusNoContent) +} + +func (core *Core) GetSubject(c *fiber.Ctx) error { + sid := c.Params("subjectId") + sessionPrincipal := GetSessionPrincipal(c) + subject, err := core.vault.GetSubject(c.Context(), sessionPrincipal, sid) + + if err != nil { + return err + } + return c.Status(http.StatusOK).JSON(subject) +} diff --git a/simulator/client.py b/simulator/client.py index af45b95..252c0b0 100644 --- a/simulator/client.py +++ b/simulator/client.py @@ -193,3 +193,20 @@ def detokenise( ) check_expected_status(response, expected_statuses) return response.json() + + def create_subject(self, eid: str, expected_statuses: Optional[list[int]] = None): + response = requests.post( + f"{self.vault_url}/subjects", + auth=(self.username, self.password), + json={"eid": eid}, + ) + check_expected_status(response, expected_statuses) + return response.json() + + def get_subject(self, sid: str, expected_statuses: Optional[list[int]] = None): + response = requests.get( + f"{self.vault_url}/subjects/{sid}", + auth=(self.username, self.password), + ) + check_expected_status(response, expected_statuses) + return response.json() diff --git a/vault/sql.go b/vault/sql.go index e7c292b..00c6f6c 100644 --- a/vault/sql.go +++ b/vault/sql.go @@ -42,6 +42,7 @@ func (st *SqlStore) CreateSchemas() error { "principal_policies": "CREATE TABLE IF NOT EXISTS principal_policies (principal_id TEXT, policy_id TEXT, UNIQUE(principal_id, policy_id))", "tokens": "CREATE TABLE IF NOT EXISTS tokens (id TEXT PRIMARY KEY, value TEXT)", "collection_metadata": "CREATE TABLE IF NOT EXISTS collection_metadata (id TEXT PRIMARY KEY, name TEXT UNIQUE, field_schema JSON)", + "subjects": "CREATE TABLE IF NOT EXISTS subjects (id TEXT PRIMARY KEY, sid TEXT UNIQUE, metadata JSON)", } for _, query := range tables { @@ -93,6 +94,11 @@ func (st *SqlStore) CreateCollection(ctx context.Context, c *Collection) error { for fieldName := range c.Fields { queryBuilder.WriteString(", " + fieldName + " TEXT") } + queryBuilder.WriteString(", sid TEXT") + queryBuilder.WriteString(", CONSTRAINT fk_sid FOREIGN KEY (sid) REFERENCES subjects(id)") + + // TODO: Add if cascade subject deletes... + queryBuilder.WriteString("ON DELETE CASCADE") queryBuilder.WriteString(")") _, err = tx.ExecContext(ctx, queryBuilder.String()) if err != nil { @@ -634,6 +640,9 @@ func (st SqlStore) Flush(ctx context.Context) error { return err } for _, table := range tables { + if table == "subjects" { + continue + } _, err = st.db.ExecContext(ctx, "DROP TABLE IF EXISTS "+table) if err != nil { return err @@ -645,3 +654,25 @@ func (st SqlStore) Flush(ctx context.Context) error { } return nil } + +func (st SqlStore) CreateSubject(ctx context.Context, subject *Subject) error { + _, err := st.db.ExecContext(ctx, "INSERT INTO subjects (id, eid, metadata) VALUES ($1, $2, $3)", subject.Id, subject.Eid, subject.Metadata) + return err +} + +func (st SqlStore) GetSubject(ctx context.Context, id string) (*Subject, error) { + var subject Subject + err := st.db.GetContext(ctx, &subject, "SELECT * FROM subjects WHERE id = $1", id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, &NotFoundError{"subject", id} + } + return nil, err + } + return &subject, nil +} + +func (st SqlStore) DeleteSubject(ctx context.Context, id string) error { + _, err := st.db.ExecContext(ctx, "DELETE FROM subjects WHERE id = $1", id) + return err +} diff --git a/vault/vault.go b/vault/vault.go index 9b4a703..da7107a 100644 --- a/vault/vault.go +++ b/vault/vault.go @@ -24,6 +24,14 @@ type Collection struct { Description string `json:"description"` } +type Subject struct { + Id string `json:"id"` + Eid string `json:"eid" validate:"required"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Metadata string `json:"metadata"` +} + type Record map[string]string // field name -> value type Privatiser interface { @@ -98,6 +106,7 @@ const ( PRINCIPALS_PPATH = "/principals" RECORDS_PPATH = "/records" POLICIES_PPATH = "/policies" + SUBJECTS_PPATH = "/subjects" ) type VaultDB interface { @@ -120,6 +129,9 @@ type VaultDB interface { CreateToken(ctx context.Context, tokenId string, value string) error DeleteToken(ctx context.Context, tokenId string) error GetTokenValue(ctx context.Context, tokenId string) (string, error) + CreateSubject(ctx context.Context, subject *Subject) error + GetSubject(ctx context.Context, subjectId string) (*Subject, error) + DeleteSubject(ctx context.Context, subjectId string) error Flush(ctx context.Context) error } @@ -669,3 +681,56 @@ func (vault *Vault) Validate(payload interface{}) error { } return ValidationErrors{errors} } + +func (vault *Vault) CreateSubject(ctx context.Context, actor Principal, subject *Subject) error { + request := Request{actor, PolicyActionWrite, SUBJECTS_PPATH} + allowed, err := vault.ValidateAction(ctx, request) + if err != nil { + return err + } + if !allowed { + return &ForbiddenError{request} + } + subject.Id = GenerateId("sub") + subject.CreatedAt = time.Now().Format(time.RFC3339) + + if err := vault.Validate(subject); err != nil { + return err + } + + err = vault.Db.CreateSubject(ctx, subject) + if err != nil { + return err + } + return nil +} + +func (vault *Vault) GetSubject(ctx context.Context, actor Principal, subjectId string) (*Subject, error) { + request := Request{actor, PolicyActionRead, fmt.Sprintf("%s/%s", SUBJECTS_PPATH, subjectId)} + allowed, err := vault.ValidateAction(ctx, request) + if err != nil { + return nil, err + } + if !allowed { + return nil, &ForbiddenError{request} + } + + return vault.Db.GetSubject(ctx, subjectId) +} + +func (vault *Vault) DeleteSubject(ctx context.Context, actor Principal, subjectId string) error { + request := Request{actor, PolicyActionWrite, fmt.Sprintf("%s/%s", SUBJECTS_PPATH, subjectId)} + allowed, err := vault.ValidateAction(ctx, request) + if err != nil { + return err + } + if !allowed { + return &ForbiddenError{request} + } + + err = vault.Db.DeleteSubject(ctx, subjectId) + if err != nil { + return err + } + return nil +}