-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #63 from mutablelogic/v4
Auth and Token Handler Updates
- Loading branch information
Showing
13 changed files
with
932 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.