Skip to content

Commit 638720d

Browse files
authored
Merge pull request #63 from mutablelogic/v4
Auth and Token Handler Updates
2 parents fe7801c + bed6bd2 commit 638720d

File tree

13 files changed

+932
-42
lines changed

13 files changed

+932
-42
lines changed

cmd/nginx-server/main.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import (
77
"os"
88
"path/filepath"
99
"syscall"
10+
"time"
1011

1112
// Packages
1213
server "github.com/mutablelogic/go-server"
1314
ctx "github.com/mutablelogic/go-server/pkg/context"
15+
auth "github.com/mutablelogic/go-server/pkg/handler/auth"
1416
nginx "github.com/mutablelogic/go-server/pkg/handler/nginx"
1517
router "github.com/mutablelogic/go-server/pkg/handler/router"
18+
tokenjar "github.com/mutablelogic/go-server/pkg/handler/tokenjar"
1619
httpserver "github.com/mutablelogic/go-server/pkg/httpserver"
1720
logger "github.com/mutablelogic/go-server/pkg/middleware/logger"
1821
provider "github.com/mutablelogic/go-server/pkg/provider"
@@ -46,6 +49,24 @@ func main() {
4649
log.Fatal(err)
4750
}
4851

52+
// Token Jar
53+
jar, err := tokenjar.Config{
54+
DataPath: n.(nginx.Nginx).Config(),
55+
WriteInterval: 30 * time.Second,
56+
}.New()
57+
if err != nil {
58+
log.Fatal(err)
59+
}
60+
61+
// Auth handler
62+
auth, err := auth.Config{
63+
TokenJar: jar.(auth.TokenJar),
64+
TokenBytes: 8,
65+
}.New()
66+
if err != nil {
67+
log.Fatal(err)
68+
}
69+
4970
// Location of the FCGI unix socket
5071
socket := filepath.Join(n.(nginx.Nginx).Config(), "run/go-server.sock")
5172

@@ -58,6 +79,12 @@ func main() {
5879
logger.(server.Middleware),
5980
},
6081
},
82+
"auth": { // /api/auth/...
83+
Service: auth.(server.ServiceEndpoints),
84+
Middleware: []server.Middleware{
85+
logger.(server.Middleware),
86+
},
87+
},
6188
},
6289
}.New()
6390
if err != nil {
@@ -75,7 +102,7 @@ func main() {
75102
}
76103

77104
// Run until we receive an interrupt
78-
provider := provider.NewProvider(logger, n, router, httpserver)
105+
provider := provider.NewProvider(logger, n, jar, auth, router, httpserver)
79106
provider.Print(ctx, "Press CTRL+C to exit")
80107
if err := provider.Run(ctx); err != nil {
81108
log.Fatal(err)

pkg/handler/auth/config.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package auth
2+
3+
import (
4+
// Packages
5+
server "github.com/mutablelogic/go-server"
6+
)
7+
8+
////////////////////////////////////////////////////////////////////////////
9+
// TYPES
10+
11+
type Config struct {
12+
TokenJar TokenJar `hcl:"token_jar" description:"Persistent storage for tokens"`
13+
TokenBytes int `hcl:"token_bytes" description:"Number of bytes in a token"`
14+
}
15+
16+
// Check interfaces are satisfied
17+
var _ server.Plugin = Config{}
18+
19+
////////////////////////////////////////////////////////////////////////////
20+
// GLOBALS
21+
22+
const (
23+
defaultName = "auth-handler"
24+
defaultTokenBytes = 16
25+
defaultRootNme = "root"
26+
)
27+
28+
///////////////////////////////////////////////////////////////////////////////
29+
// LIFECYCLE
30+
31+
// Name returns the name of the service
32+
func (Config) Name() string {
33+
return defaultName
34+
}
35+
36+
// Description returns the description of the service
37+
func (Config) Description() string {
38+
return "token and group management for authentication and authorisation"
39+
}
40+
41+
// Create a new task from the configuration
42+
func (c Config) New() (server.Task, error) {
43+
return New(c)
44+
}

pkg/handler/auth/endpoints.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"regexp"
7+
"strings"
8+
"time"
9+
10+
// Packages
11+
server "github.com/mutablelogic/go-server"
12+
router "github.com/mutablelogic/go-server/pkg/handler/router"
13+
httprequest "github.com/mutablelogic/go-server/pkg/httprequest"
14+
httpresponse "github.com/mutablelogic/go-server/pkg/httpresponse"
15+
)
16+
17+
///////////////////////////////////////////////////////////////////////////////
18+
// GLOBALS
19+
20+
const (
21+
jsonIndent = 2
22+
23+
// Token should be at least eight bytes (16 chars)
24+
reTokenString = `[a-zA-Z0-9]{16}[a-zA-Z0-9]*`
25+
)
26+
27+
var (
28+
reRoot = regexp.MustCompile(`^/?$`)
29+
reToken = regexp.MustCompile(`^/(` + reTokenString + `)/?$`)
30+
)
31+
32+
///////////////////////////////////////////////////////////////////////////////
33+
// PUBLIC METHODS - ENDPOINTS
34+
35+
// Add endpoints to the router
36+
func (service *auth) AddEndpoints(ctx context.Context, router server.Router) {
37+
// Path: /
38+
// Methods: GET
39+
// Scopes: read // TODO: Add scopes
40+
// Description: Get current set of tokens and groups
41+
router.AddHandlerFuncRe(ctx, reRoot, service.ListTokens, http.MethodGet)
42+
43+
// Path: /
44+
// Methods: POST
45+
// Scopes: write // TODO: Add scopes
46+
// Description: Create a new token
47+
router.AddHandlerFuncRe(ctx, reRoot, service.CreateToken, http.MethodPost)
48+
49+
// Path: /<token>
50+
// Methods: GET
51+
// Scopes: read // TODO: Add scopes
52+
// Description: Get a token
53+
router.AddHandlerFuncRe(ctx, reToken, service.GetToken, http.MethodGet)
54+
55+
// Path: /<token>
56+
// Methods: DELETE, PATCH
57+
// Scopes: write // TODO: Add scopes
58+
// Description: Delete or update a token
59+
router.AddHandlerFuncRe(ctx, reToken, service.UpdateToken, http.MethodDelete, http.MethodPatch)
60+
}
61+
62+
///////////////////////////////////////////////////////////////////////////////
63+
// PUBLIC METHODS
64+
65+
// Get all tokens
66+
func (service *auth) ListTokens(w http.ResponseWriter, r *http.Request) {
67+
tokens := service.jar.Tokens()
68+
result := make([]*Token, 0, len(tokens))
69+
for _, token := range tokens {
70+
token.Value = ""
71+
result = append(result, &token)
72+
}
73+
httpresponse.JSON(w, result, http.StatusOK, jsonIndent)
74+
}
75+
76+
// Get a token
77+
func (service *auth) GetToken(w http.ResponseWriter, r *http.Request) {
78+
urlParameters := router.Params(r.Context())
79+
token := service.jar.GetWithValue(strings.ToLower(urlParameters[0]))
80+
if token.IsZero() {
81+
httpresponse.Error(w, http.StatusNotFound)
82+
return
83+
}
84+
85+
// Remove the token value before returning
86+
token.Value = ""
87+
88+
// Return the token
89+
httpresponse.JSON(w, token, http.StatusOK, jsonIndent)
90+
}
91+
92+
// Create a token
93+
func (service *auth) CreateToken(w http.ResponseWriter, r *http.Request) {
94+
var req TokenCreate
95+
96+
// Get the request
97+
if err := httprequest.Read(r, &req); err != nil {
98+
httpresponse.Error(w, http.StatusBadRequest, err.Error())
99+
return
100+
}
101+
102+
// Check for a valid name
103+
req.Name = strings.TrimSpace(req.Name)
104+
if req.Name == "" {
105+
httpresponse.Error(w, http.StatusBadRequest, "missing 'name'")
106+
} else if token := service.jar.GetWithName(req.Name); token.IsValid() {
107+
httpresponse.Error(w, http.StatusConflict, "duplicate 'name'")
108+
}
109+
110+
// Create the token
111+
token := NewToken(req.Name, service.tokenBytes, req.Duration.Duration, req.Scope...)
112+
if !token.IsValid() {
113+
httpresponse.Error(w, http.StatusInternalServerError)
114+
return
115+
}
116+
117+
// Add the token
118+
if err := service.jar.Create(token); err != nil {
119+
httpresponse.Error(w, http.StatusInternalServerError, err.Error())
120+
return
121+
}
122+
123+
// Remove the access_time which doesn't make sense when
124+
// creating a token
125+
token.Time = time.Time{}
126+
127+
// Return the token
128+
httpresponse.JSON(w, token, http.StatusCreated, jsonIndent)
129+
}
130+
131+
// Update an existing token
132+
func (service *auth) UpdateToken(w http.ResponseWriter, r *http.Request) {
133+
urlParameters := router.Params(r.Context())
134+
token := service.jar.GetWithValue(strings.ToLower(urlParameters[0]))
135+
if token.IsZero() {
136+
httpresponse.Error(w, http.StatusNotFound)
137+
return
138+
}
139+
140+
switch r.Method {
141+
case http.MethodDelete:
142+
if err := service.jar.Delete(token.Value); err != nil {
143+
httpresponse.Error(w, http.StatusInternalServerError, err.Error())
144+
return
145+
}
146+
default:
147+
// TODO: PATCH
148+
// Patch can be with name, expire_time, scopes
149+
httpresponse.Error(w, http.StatusMethodNotAllowed)
150+
return
151+
}
152+
153+
// Respond with no content
154+
httpresponse.Empty(w, http.StatusOK)
155+
}

pkg/handler/auth/interface.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
)
6+
7+
type TokenJar interface {
8+
// Run the token jar until cancelled
9+
Run(context.Context) error
10+
11+
// Return all tokens
12+
Tokens() []Token
13+
14+
// Return a token from the jar by value, or an invalid token
15+
// if the token is not found. The method should update the access
16+
// time of the token.
17+
GetWithValue(string) Token
18+
19+
// Return a token from the jar by name, or nil if the token
20+
// is not found. The method should not update the access time
21+
// of the token.
22+
GetWithName(string) Token
23+
24+
// Put a token into the jar, assuming it does not yet exist.
25+
Create(Token) error
26+
27+
// Update an existing token in the jar, assuming it already exists.
28+
Update(Token) error
29+
30+
// Remove a token from the jar, based on key.
31+
Delete(string) error
32+
}

pkg/handler/auth/scope.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package auth
2+
3+
import "github.com/mutablelogic/go-server/pkg/version"
4+
5+
var (
6+
// Root scope allows ANY operation
7+
ScopeRoot = version.GitSource + "scope/root"
8+
)

pkg/handler/auth/task.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
6+
// Packages
7+
server "github.com/mutablelogic/go-server"
8+
"github.com/mutablelogic/go-server/pkg/provider"
9+
10+
// Namespace imports
11+
. "github.com/djthorpe/go-errors"
12+
)
13+
14+
///////////////////////////////////////////////////////////////////////////////
15+
// TYPES
16+
17+
type auth struct {
18+
jar TokenJar
19+
tokenBytes int
20+
}
21+
22+
// Check interfaces are satisfied
23+
var _ server.Task = (*auth)(nil)
24+
var _ server.ServiceEndpoints = (*auth)(nil)
25+
26+
///////////////////////////////////////////////////////////////////////////////
27+
// LIFECYCLE
28+
29+
// Create a new auth task from the configuration
30+
func New(c Config) (*auth, error) {
31+
task := new(auth)
32+
33+
// Set token jar
34+
if c.TokenJar == nil {
35+
return nil, ErrInternalAppError.With("missing 'tokenjar'")
36+
} else {
37+
task.jar = c.TokenJar
38+
}
39+
40+
// Set token bytes
41+
if c.TokenBytes <= 0 {
42+
task.tokenBytes = defaultTokenBytes
43+
} else {
44+
task.tokenBytes = c.TokenBytes
45+
}
46+
47+
// Return success
48+
return task, nil
49+
}
50+
51+
/////////////////////////////////////////////////////////////////////
52+
// PUBLIC METHODS
53+
54+
// Return the label
55+
func (task *auth) Label() string {
56+
// TODO
57+
return defaultName
58+
}
59+
60+
// Run the task until the context is cancelled
61+
func (task *auth) Run(ctx context.Context) error {
62+
var result error
63+
64+
// Logger
65+
logger := provider.Logger(ctx)
66+
67+
// If there are no tokens, then create a "root" token
68+
if tokens := task.jar.Tokens(); len(tokens) == 0 {
69+
token := NewToken(defaultRootNme, task.tokenBytes, 0, ScopeRoot)
70+
logger.Printf(ctx, "Creating root token %q for scope %q", token.Value, ScopeRoot)
71+
if err := task.jar.Create(token); err != nil {
72+
return err
73+
}
74+
}
75+
76+
// Run the task until cancelled
77+
<-ctx.Done()
78+
79+
// Return any errors
80+
return result
81+
}

0 commit comments

Comments
 (0)