Skip to content

Commit

Permalink
Merge pull request #63 from mutablelogic/v4
Browse files Browse the repository at this point in the history
Auth and Token Handler Updates
  • Loading branch information
djthorpe authored Jun 4, 2024
2 parents fe7801c + bed6bd2 commit 638720d
Show file tree
Hide file tree
Showing 13 changed files with 932 additions and 42 deletions.
29 changes: 28 additions & 1 deletion cmd/nginx-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import (
"os"
"path/filepath"
"syscall"
"time"

// Packages
server "github.com/mutablelogic/go-server"
ctx "github.com/mutablelogic/go-server/pkg/context"
auth "github.com/mutablelogic/go-server/pkg/handler/auth"
nginx "github.com/mutablelogic/go-server/pkg/handler/nginx"
router "github.com/mutablelogic/go-server/pkg/handler/router"
tokenjar "github.com/mutablelogic/go-server/pkg/handler/tokenjar"
httpserver "github.com/mutablelogic/go-server/pkg/httpserver"
logger "github.com/mutablelogic/go-server/pkg/middleware/logger"
provider "github.com/mutablelogic/go-server/pkg/provider"
Expand Down Expand Up @@ -46,6 +49,24 @@ func main() {
log.Fatal(err)
}

// Token Jar
jar, err := tokenjar.Config{
DataPath: n.(nginx.Nginx).Config(),
WriteInterval: 30 * time.Second,
}.New()
if err != nil {
log.Fatal(err)
}

// Auth handler
auth, err := auth.Config{
TokenJar: jar.(auth.TokenJar),
TokenBytes: 8,
}.New()
if err != nil {
log.Fatal(err)
}

// Location of the FCGI unix socket
socket := filepath.Join(n.(nginx.Nginx).Config(), "run/go-server.sock")

Expand All @@ -58,6 +79,12 @@ func main() {
logger.(server.Middleware),
},
},
"auth": { // /api/auth/...
Service: auth.(server.ServiceEndpoints),
Middleware: []server.Middleware{
logger.(server.Middleware),
},
},
},
}.New()
if err != nil {
Expand All @@ -75,7 +102,7 @@ func main() {
}

// Run until we receive an interrupt
provider := provider.NewProvider(logger, n, router, httpserver)
provider := provider.NewProvider(logger, n, jar, auth, router, httpserver)
provider.Print(ctx, "Press CTRL+C to exit")
if err := provider.Run(ctx); err != nil {
log.Fatal(err)
Expand Down
44 changes: 44 additions & 0 deletions pkg/handler/auth/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package auth

import (
// Packages
server "github.com/mutablelogic/go-server"
)

////////////////////////////////////////////////////////////////////////////
// TYPES

type Config struct {
TokenJar TokenJar `hcl:"token_jar" description:"Persistent storage for tokens"`
TokenBytes int `hcl:"token_bytes" description:"Number of bytes in a token"`
}

// Check interfaces are satisfied
var _ server.Plugin = Config{}

////////////////////////////////////////////////////////////////////////////
// GLOBALS

const (
defaultName = "auth-handler"
defaultTokenBytes = 16
defaultRootNme = "root"
)

///////////////////////////////////////////////////////////////////////////////
// LIFECYCLE

// Name returns the name of the service
func (Config) Name() string {
return defaultName
}

// Description returns the description of the service
func (Config) Description() string {
return "token and group management for authentication and authorisation"
}

// Create a new task from the configuration
func (c Config) New() (server.Task, error) {
return New(c)
}
155 changes: 155 additions & 0 deletions pkg/handler/auth/endpoints.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package auth

import (
"context"
"net/http"
"regexp"
"strings"
"time"

// Packages
server "github.com/mutablelogic/go-server"
router "github.com/mutablelogic/go-server/pkg/handler/router"
httprequest "github.com/mutablelogic/go-server/pkg/httprequest"
httpresponse "github.com/mutablelogic/go-server/pkg/httpresponse"
)

///////////////////////////////////////////////////////////////////////////////
// GLOBALS

const (
jsonIndent = 2

// Token should be at least eight bytes (16 chars)
reTokenString = `[a-zA-Z0-9]{16}[a-zA-Z0-9]*`
)

var (
reRoot = regexp.MustCompile(`^/?$`)
reToken = regexp.MustCompile(`^/(` + reTokenString + `)/?$`)
)

///////////////////////////////////////////////////////////////////////////////
// PUBLIC METHODS - ENDPOINTS

// Add endpoints to the router
func (service *auth) AddEndpoints(ctx context.Context, router server.Router) {
// Path: /
// Methods: GET
// Scopes: read // TODO: Add scopes
// Description: Get current set of tokens and groups
router.AddHandlerFuncRe(ctx, reRoot, service.ListTokens, http.MethodGet)

// Path: /
// Methods: POST
// Scopes: write // TODO: Add scopes
// Description: Create a new token
router.AddHandlerFuncRe(ctx, reRoot, service.CreateToken, http.MethodPost)

// Path: /<token>
// Methods: GET
// Scopes: read // TODO: Add scopes
// Description: Get a token
router.AddHandlerFuncRe(ctx, reToken, service.GetToken, http.MethodGet)

// Path: /<token>
// Methods: DELETE, PATCH
// Scopes: write // TODO: Add scopes
// Description: Delete or update a token
router.AddHandlerFuncRe(ctx, reToken, service.UpdateToken, http.MethodDelete, http.MethodPatch)
}

///////////////////////////////////////////////////////////////////////////////
// PUBLIC METHODS

// Get all tokens
func (service *auth) ListTokens(w http.ResponseWriter, r *http.Request) {
tokens := service.jar.Tokens()
result := make([]*Token, 0, len(tokens))
for _, token := range tokens {
token.Value = ""
result = append(result, &token)
}
httpresponse.JSON(w, result, http.StatusOK, jsonIndent)
}

// Get a token
func (service *auth) GetToken(w http.ResponseWriter, r *http.Request) {
urlParameters := router.Params(r.Context())
token := service.jar.GetWithValue(strings.ToLower(urlParameters[0]))
if token.IsZero() {
httpresponse.Error(w, http.StatusNotFound)
return
}

// Remove the token value before returning
token.Value = ""

// Return the token
httpresponse.JSON(w, token, http.StatusOK, jsonIndent)
}

// Create a token
func (service *auth) CreateToken(w http.ResponseWriter, r *http.Request) {
var req TokenCreate

// Get the request
if err := httprequest.Read(r, &req); err != nil {
httpresponse.Error(w, http.StatusBadRequest, err.Error())
return
}

// Check for a valid name
req.Name = strings.TrimSpace(req.Name)
if req.Name == "" {
httpresponse.Error(w, http.StatusBadRequest, "missing 'name'")
} else if token := service.jar.GetWithName(req.Name); token.IsValid() {
httpresponse.Error(w, http.StatusConflict, "duplicate 'name'")
}

// Create the token
token := NewToken(req.Name, service.tokenBytes, req.Duration.Duration, req.Scope...)
if !token.IsValid() {
httpresponse.Error(w, http.StatusInternalServerError)
return
}

// Add the token
if err := service.jar.Create(token); err != nil {
httpresponse.Error(w, http.StatusInternalServerError, err.Error())
return
}

// Remove the access_time which doesn't make sense when
// creating a token
token.Time = time.Time{}

// Return the token
httpresponse.JSON(w, token, http.StatusCreated, jsonIndent)
}

// Update an existing token
func (service *auth) UpdateToken(w http.ResponseWriter, r *http.Request) {
urlParameters := router.Params(r.Context())
token := service.jar.GetWithValue(strings.ToLower(urlParameters[0]))
if token.IsZero() {
httpresponse.Error(w, http.StatusNotFound)
return
}

switch r.Method {
case http.MethodDelete:
if err := service.jar.Delete(token.Value); err != nil {
httpresponse.Error(w, http.StatusInternalServerError, err.Error())
return
}
default:
// TODO: PATCH
// Patch can be with name, expire_time, scopes
httpresponse.Error(w, http.StatusMethodNotAllowed)
return
}

// Respond with no content
httpresponse.Empty(w, http.StatusOK)
}
32 changes: 32 additions & 0 deletions pkg/handler/auth/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package auth

import (
"context"
)

type TokenJar interface {
// Run the token jar until cancelled
Run(context.Context) error

// Return all tokens
Tokens() []Token

// Return a token from the jar by value, or an invalid token
// if the token is not found. The method should update the access
// time of the token.
GetWithValue(string) Token

// Return a token from the jar by name, or nil if the token
// is not found. The method should not update the access time
// of the token.
GetWithName(string) Token

// Put a token into the jar, assuming it does not yet exist.
Create(Token) error

// Update an existing token in the jar, assuming it already exists.
Update(Token) error

// Remove a token from the jar, based on key.
Delete(string) error
}
8 changes: 8 additions & 0 deletions pkg/handler/auth/scope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package auth

import "github.com/mutablelogic/go-server/pkg/version"

var (
// Root scope allows ANY operation
ScopeRoot = version.GitSource + "scope/root"
)
81 changes: 81 additions & 0 deletions pkg/handler/auth/task.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package auth

import (
"context"

// Packages
server "github.com/mutablelogic/go-server"
"github.com/mutablelogic/go-server/pkg/provider"

// Namespace imports
. "github.com/djthorpe/go-errors"
)

///////////////////////////////////////////////////////////////////////////////
// TYPES

type auth struct {
jar TokenJar
tokenBytes int
}

// Check interfaces are satisfied
var _ server.Task = (*auth)(nil)
var _ server.ServiceEndpoints = (*auth)(nil)

///////////////////////////////////////////////////////////////////////////////
// LIFECYCLE

// Create a new auth task from the configuration
func New(c Config) (*auth, error) {
task := new(auth)

// Set token jar
if c.TokenJar == nil {
return nil, ErrInternalAppError.With("missing 'tokenjar'")
} else {
task.jar = c.TokenJar
}

// Set token bytes
if c.TokenBytes <= 0 {
task.tokenBytes = defaultTokenBytes
} else {
task.tokenBytes = c.TokenBytes
}

// Return success
return task, nil
}

/////////////////////////////////////////////////////////////////////
// PUBLIC METHODS

// Return the label
func (task *auth) Label() string {
// TODO
return defaultName
}

// Run the task until the context is cancelled
func (task *auth) Run(ctx context.Context) error {
var result error

// Logger
logger := provider.Logger(ctx)

// If there are no tokens, then create a "root" token
if tokens := task.jar.Tokens(); len(tokens) == 0 {
token := NewToken(defaultRootNme, task.tokenBytes, 0, ScopeRoot)
logger.Printf(ctx, "Creating root token %q for scope %q", token.Value, ScopeRoot)
if err := task.jar.Create(token); err != nil {
return err
}
}

// Run the task until cancelled
<-ctx.Done()

// Return any errors
return result
}
Loading

0 comments on commit 638720d

Please sign in to comment.