Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth and Token Handler Updates #63

Merged
merged 10 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading