Skip to content

Commit

Permalink
add declaration "touch" which resolves #33. also begin work on OpenAP…
Browse files Browse the repository at this point in the history
…I docs #4.
  • Loading branch information
jessepeterson committed Jun 28, 2023
1 parent 92fac01 commit 33375ef
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 20 deletions.
8 changes: 7 additions & 1 deletion cmd/kmfddm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func main() {
logger.Info("msg", "empty API key; API disabled")
}

storage, err := storage(*flStorage, *flDSN)
storage, err := setupStorage(*flStorage, *flDSN)
if err != nil {
logger.Info("msg", "init storage", "name", *flStorage, "err", err)
os.Exit(1)
Expand Down Expand Up @@ -150,6 +150,12 @@ func main() {
"DELETE",
)

mux.Handle(
"/v1/declarations/:id/touch",
apihttp.TouchDeclarationHandler(storage, nanoNotif, logger.With("handler", "touch-declaration")),
"POST",
)

// sets
mux.Handle(
"/v1/sets",
Expand Down
4 changes: 3 additions & 1 deletion cmd/kmfddm/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/jessepeterson/kmfddm/http/api"
"github.com/jessepeterson/kmfddm/http/ddm"
"github.com/jessepeterson/kmfddm/notifier"
"github.com/jessepeterson/kmfddm/storage"
"github.com/jessepeterson/kmfddm/storage/file"
"github.com/jessepeterson/kmfddm/storage/mysql"

Expand All @@ -23,11 +24,12 @@ type allStorage interface {
ddm.StatusStorage
api.EnrollmentAPIStorage
api.StatusAPIStorage
storage.Toucher
}

var hasher func() hash.Hash = func() hash.Hash { return xxhash.New() }

func storage(name, dsn string) (allStorage, error) {
func setupStorage(name, dsn string) (allStorage, error) {
switch name {
case "mysql":
return mysql.New(
Expand Down
92 changes: 92 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
openapi: 3.0.0
info:
version: 0.1.0
title: KMFDDM server API
servers:
- url: http://[::1]:9002/
paths:
/version:
get:
description: Returns the running KMFDDM server version
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
version:
type: string
example: "v0.1.0"
/v1/declarations/{id}/touch:
post:
description: Updates a declaration's `ServerToken` only.
security:
- basicAuth: []
responses:
'204':
description: Declaration server token successfully updated.
'401':
$ref: '#/components/responses/UnauthorizedError'
'400':
$ref: '#/components/responses/JSONBadRequest'
'500':
$ref: '#/components/responses/JSONError'
parameters:
- $ref: '#/components/parameters/declarationID'
components:
parameters:
declarationID:
name: id
in: path
description: Identifier of the declaration.
required: true
style: simple
schema:
type: string
example: 'com.example.test'
securitySchemes:
basicAuth:
type: http
scheme: basic
responses:
UnauthorizedError:
description: API key is missing or invalid.
headers:
WWW-Authenticate:
schema:
type: string
BadRequest:
description: There was a problem with the supplied request. The request was in an incorrect format or other request data error. See server logs for more information.
content:
text/plain:
schema:
type: string
example: Bad Request
Error:
description: An internal server error occured on this endpoint. See server logs for more information.
content:
text/plain:
schema:
type: string
example: Internal Server Error
JSONBadRequest:
description: There was a problem with the supplied request. The request was in an incorrect format or other request data error.
content:
application/json:
schema:
$ref: '#/components/schemas/JSONError'
JSONError:
description: An internal server error occured on this endpoint.
content:
application/json:
schema:
$ref: '#/components/schemas/JSONError'
schemas:
JSONError:
type: object
properties:
error:
type: string
example: "it was sunny outside"
27 changes: 27 additions & 0 deletions http/api/declarations.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/jessepeterson/kmfddm/ddm"
"github.com/jessepeterson/kmfddm/log"
"github.com/jessepeterson/kmfddm/log/ctxlog"
"github.com/jessepeterson/kmfddm/storage"
)

type DeclarationAPIStorage interface {
Expand Down Expand Up @@ -149,3 +150,29 @@ func GetDeclarationsHandler(store DeclarationAPIStorage, logger log.Logger) http
}
}
}

// TouchDeclarationHandler touches a declaration specified by ID.
func TouchDeclarationHandler(store storage.Toucher, notifier Notifier, logger log.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logger := ctxlog.Logger(r.Context(), logger)
var err error
declarationID := getResourceID(r)
if declarationID == "" {
err = errors.New("empty declaration identifier")
jsonErrorAndLog(w, http.StatusBadRequest, err, "validating input", logger)
return
}
logger = logger.With("declaration", declarationID)
err = store.TouchDeclaration(r.Context(), declarationID)
if err != nil {
jsonErrorAndLog(w, http.StatusInternalServerError, err, "touching declaration", logger)
return
}
http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent)
err = notifier.DeclarationChanged(r.Context(), declarationID)
if err != nil {
logger.Info("msg", "notifying", "err", err)
return
}
}
}
47 changes: 37 additions & 10 deletions storage/file/declarations.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ func (s *File) StoreDeclaration(_ context.Context, d *ddm.Declaration) (bool, er
s.mu.Lock()
defer s.mu.Unlock()

return s.writeDeclarationFiles(d, false)
}

func (s *File) writeDeclarationFiles(d *ddm.Declaration, forceNewSalt bool) (bool, error) {
var err error
var token string
var creationSalt []byte
Expand All @@ -34,21 +38,25 @@ func (s *File) StoreDeclaration(_ context.Context, d *ddm.Declaration) (bool, er
// try to read token and cache our existing token if it exists
if tokenBytes, err := os.ReadFile(tokenFilename); errors.Is(err, os.ErrNotExist) {
tokenMissing = true
// token is missing, lets make a new salt
if creationSalt, err = newSalt(); err != nil {
return false, fmt.Errorf("creating new salt: %w", err)
}
} else if err != nil {
return false, fmt.Errorf("reading server token: %w", err)
} else {
// found the token, lets convert it (and read our salt)
token = string(tokenBytes)
if creationSalt, err = os.ReadFile(saltFilename); err != nil {
return false, fmt.Errorf("reading creation salt: %w", err)
if !forceNewSalt {
if creationSalt, err = os.ReadFile(saltFilename); err != nil {
return false, fmt.Errorf("reading creation salt: %w", err)
}
}
}

if tokenMissing || forceNewSalt {
if creationSalt, err = newSalt(); err != nil {
return false, fmt.Errorf("creating new salt: %w", err)
}
}

// unmarshal the uploaded declaration
// unmarshal the raw declaration
var declaration map[string]interface{}
if err = json.Unmarshal(d.Raw, &declaration); err != nil {
return false, err
Expand All @@ -63,7 +71,7 @@ func (s *File) StoreDeclaration(_ context.Context, d *ddm.Declaration) (bool, er
return false, fmt.Errorf("marshaling no-token declaration: %w", err)
}

// hash the marshaled declaration (without token and with creation salt)
// hash the marshaled declaration (again without token but with creation salt)
hasher := s.newHash()
_, err = hasher.Write(append(dBytes, creationSalt...))
if err != nil {
Expand Down Expand Up @@ -95,8 +103,9 @@ func (s *File) StoreDeclaration(_ context.Context, d *ddm.Declaration) (bool, er
return false, fmt.Errorf("writing declaration token: %w", err)
}

if tokenMissing {
// we only want to change the salt if we're making a "new" declaration
if tokenMissing || forceNewSalt {
// we only want to change the salt if we're either touching or
// making a "new" declaration.
if err = os.WriteFile(saltFilename, creationSalt, 0644); err != nil {
return false, fmt.Errorf("writing creation salt: %w", err)
}
Expand All @@ -114,6 +123,11 @@ func (s *File) StoreDeclaration(_ context.Context, d *ddm.Declaration) (bool, er
func (s *File) RetrieveDeclaration(_ context.Context, declarationID string) (*ddm.Declaration, error) {
s.mu.RLock()
defer s.mu.RUnlock()

return s.readDeclarationFile(declarationID)
}

func (s *File) readDeclarationFile(declarationID string) (*ddm.Declaration, error) {
dBytes, err := os.ReadFile(s.declarationFilename(declarationID))
if err != nil {
return nil, fmt.Errorf("reading declaration: %w", err)
Expand Down Expand Up @@ -171,3 +185,16 @@ func (s *File) RetrieveDeclarations(_ context.Context) ([]string, error) {
}
return truncated, nil
}

// TouchDeclaration rewrites a declaration with a new ServerToken.
func (s *File) TouchDeclaration(ctx context.Context, declarationID string) error {
s.mu.Lock()
defer s.mu.Unlock()

d, err := s.readDeclarationFile(declarationID)
if err != nil {
return err
}
_, err = s.writeDeclarationFiles(d, true)
return err
}
30 changes: 27 additions & 3 deletions storage/mysql/declarations.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package mysql

import (
"context"
"errors"
"fmt"
"strings"

"github.com/jessepeterson/kmfddm/ddm"
)

var ErrDeclarationNotChanged = errors.New("declaration not changed (may not exist)")

func (s *MySQLStorage) StoreDeclaration(ctx context.Context, d *ddm.Declaration) (bool, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
Expand Down Expand Up @@ -122,10 +125,11 @@ WHERE
return
}

func (s *MySQLStorage) DeleteDeclaration(ctx context.Context, identifier string) (bool, error) {
func (s *MySQLStorage) DeleteDeclaration(ctx context.Context, declarationID string) (bool, error) {
result, err := s.db.ExecContext(
ctx, `DELETE FROM declarations WHERE identifier = ?;`,
identifier,
ctx,
`DELETE FROM declarations WHERE identifier = ?;`,
declarationID,
)
if err != nil {
return false, err
Expand All @@ -147,3 +151,23 @@ func (s *MySQLStorage) RetrieveDeclarations(ctx context.Context) ([]string, erro
`SELECT identifier FROM declarations;`,
)
}

// TouchDeclaration updates a declaration's "touch count" which makes a new server token.
func (s *MySQLStorage) TouchDeclaration(ctx context.Context, declarationID string) error {
result, err := s.db.ExecContext(
ctx,
`UPDATE declarations SET touched_ct = touched_ct + 1 WHERE identifier = ?;`,
declarationID,
)
if err != nil {
return err
}
changed, err := resultChangedRows(result)
if err != nil {
return err
}
if !changed {
return ErrDeclarationNotChanged
}
return nil
}
4 changes: 2 additions & 2 deletions storage/mysql/schema.00001.sql
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ALTER TABLE declarations ADD COLUMN touched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
ALTER TABLE declarations MODIFY COLUMN server_token CHAR(40) AS (SHA1(CONCAT(identifier, type, payload, created_at, touched_at))) STORED NOT NULL;
ALTER TABLE declarations ADD COLUMN touched_ct INT DEFAULT 0 NOT NULL;
ALTER TABLE declarations MODIFY COLUMN server_token CHAR(40) AS (SHA1(CONCAT(identifier, type, payload, created_at, touched_ct))) STORED NOT NULL;
4 changes: 2 additions & 2 deletions storage/mysql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ CREATE TABLE declarations (

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
touched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
touched_ct INT DEFAULT 0 NOT NULL,

server_token CHAR(40) AS (SHA1(CONCAT(identifier, type, payload, created_at, touched_at))) STORED NOT NULL,
server_token CHAR(40) AS (SHA1(CONCAT(identifier, type, payload, created_at, touched_ct))) STORED NOT NULL,

PRIMARY KEY (identifier),

Expand Down
11 changes: 11 additions & 0 deletions storage/storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Package storage defines shared types and interfaces for storage.
package storage

import (
"context"
)

type Toucher interface {
// TouchDeclaration forces a change only to a declaration's ServerToken.
TouchDeclaration(ctx context.Context, declarationID string) error
}
2 changes: 2 additions & 0 deletions storage/test/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/jessepeterson/kmfddm/http/api"
httpddm "github.com/jessepeterson/kmfddm/http/ddm"
"github.com/jessepeterson/kmfddm/notifier"
"github.com/jessepeterson/kmfddm/storage"
)

const testDecl = `{
Expand All @@ -23,6 +24,7 @@ type allTestStorage interface {
api.EnrollmentAPIStorage
notifier.EnrollmentIDFinder
httpddm.TokensDeclarationItemsRetriever
storage.Toucher
}

func TestBasic(t *testing.T, storage allTestStorage, ctx context.Context) {
Expand Down
Loading

0 comments on commit 33375ef

Please sign in to comment.