Skip to content

Commit

Permalink
feat: add authorization for the API layer
Browse files Browse the repository at this point in the history
Signed-off-by: Donnie Adams <[email protected]>
  • Loading branch information
thedadams committed Oct 16, 2024
1 parent 2629786 commit 17c98eb
Show file tree
Hide file tree
Showing 18 changed files with 477 additions and 362 deletions.
22 changes: 22 additions & 0 deletions pkg/api/authn/anonymous.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package authn

import (
"net/http"

"github.com/otto8-ai/otto8/pkg/api/authz"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
)

type Anonymous struct {
}

func (n Anonymous) AuthenticateRequest(*http.Request) (*authenticator.Response, bool, error) {
return &authenticator.Response{
User: &user.DefaultInfo{
UID: "anonymous",
Name: "anonymous",
Groups: []string{authz.UnauthenticatedGroup},
},
}, true, nil
}
29 changes: 29 additions & 0 deletions pkg/api/authn/authn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package authn

import (
"net/http"

"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
)

type Authenticator struct {
authenticator authenticator.Request
}

func NewAuthenticator(authenticator authenticator.Request) *Authenticator {
return &Authenticator{
authenticator: authenticator,
}
}

func (a *Authenticator) Authenticate(req *http.Request) (user.Info, error) {
resp, ok, err := a.authenticator.AuthenticateRequest(req)
if err != nil {
return nil, err
}
if !ok {
panic("authentication should always succeed")
}
return resp.User, nil
}
21 changes: 21 additions & 0 deletions pkg/api/authn/noauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package authn

import (
"net/http"

"github.com/otto8-ai/otto8/pkg/api/authz"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
)

type NoAuth struct {
}

func (n NoAuth) AuthenticateRequest(*http.Request) (*authenticator.Response, bool, error) {
return &authenticator.Response{
User: &user.DefaultInfo{
Name: "nobody",
Groups: []string{authz.AdminGroup, authz.AuthenticatedGroup},
},
}, true, nil
}
89 changes: 89 additions & 0 deletions pkg/api/authz/authz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package authz

import (
"net/http"
"slices"

"k8s.io/apiserver/pkg/authentication/user"
)

const (
AdminGroup = "admin"
AuthenticatedGroup = "authenticated"
UnauthenticatedGroup = "unauthenticated"

// anyGroup is an internal group that allows access to any group
anyGroup = "*"
)

type Authorizer struct {
rules []rule
}

func NewAuthorizer() *Authorizer {
return &Authorizer{
rules: defaultRules(),
}
}

func (a *Authorizer) Authorize(req *http.Request, user user.Info) bool {
userGroups := user.GetGroups()
for _, r := range a.rules {
if r.group == anyGroup || slices.Contains(userGroups, r.group) {
if _, pattern := r.mux.Handler(req); pattern != "" {
return true
}
}
}

return false
}

type rule struct {
group string
mux *http.ServeMux
}

func defaultRules() []rule {
var rules []rule

// Build admin mux, admins can assess any URL
adminMux := http.NewServeMux()
adminMux.Handle("/", (*fake)(nil))

rules = append(rules, rule{
group: AdminGroup,
mux: adminMux,
})

// Build mux that anyone can access
anyMux := http.NewServeMux()
anyMux.Handle("POST /api/webhooks/{id}", (*fake)(nil))

anyMux.Handle("GET /api/token-request/{id}", (*fake)(nil))
anyMux.Handle("POST /api/token-request", (*fake)(nil))
anyMux.Handle("GET /api/token-request/{id}/{service}", (*fake)(nil))

anyMux.Handle("GET /api/auth-providers", (*fake)(nil))
anyMux.Handle("GET /api/auth-providers/{slug}", (*fake)(nil))

anyMux.Handle("GET /api/oauth/start/{id}/{service}", (*fake)(nil))
anyMux.Handle("/api/oauth/redirect/{service}", (*fake)(nil))

anyMux.Handle("GET /api/app-oauth/authorize/{id}", (*fake)(nil))
anyMux.Handle("GET /api/app-oauth/refresh/{id}", (*fake)(nil))
anyMux.Handle("GET /api/app-oauth/callback/{id}", (*fake)(nil))
anyMux.Handle("GET /api/app-oauth/get-token", (*fake)(nil))

rules = append(rules, rule{
group: anyGroup,
mux: anyMux,
})

return rules
}

// fake is a fake handler that does nothing
type fake struct{}

func (f *fake) ServeHTTP(http.ResponseWriter, *http.Request) {}
9 changes: 5 additions & 4 deletions pkg/api/handlers/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/gptscript-ai/go-gptscript"
"github.com/otto8-ai/otto8/apiclient/types"
"github.com/otto8-ai/otto8/pkg/api"
"github.com/otto8-ai/otto8/pkg/api/server"
"github.com/otto8-ai/otto8/pkg/render"
v1 "github.com/otto8-ai/otto8/pkg/storage/apis/otto.gptscript.ai/v1"
"github.com/otto8-ai/otto8/pkg/system"
Expand Down Expand Up @@ -48,7 +49,7 @@ func (a *AgentHandler) Update(req api.Context) error {
return err
}

return req.Write(convertAgent(agent, api.GetURLPrefix(req)))
return req.Write(convertAgent(agent, server.GetURLPrefix(req)))
}

func (a *AgentHandler) Delete(req api.Context) error {
Expand Down Expand Up @@ -84,7 +85,7 @@ func (a *AgentHandler) Create(req api.Context) error {
}

req.WriteHeader(http.StatusCreated)
return req.Write(convertAgent(agent, api.GetURLPrefix(req)))
return req.Write(convertAgent(agent, server.GetURLPrefix(req)))
}

func convertAgent(agent v1.Agent, prefix string) *types.Agent {
Expand All @@ -109,7 +110,7 @@ func (a *AgentHandler) ByID(req api.Context) error {
return err
}

return req.Write(convertAgent(agent, api.GetURLPrefix(req)))
return req.Write(convertAgent(agent, server.GetURLPrefix(req)))
}

func (a *AgentHandler) List(req api.Context) error {
Expand All @@ -120,7 +121,7 @@ func (a *AgentHandler) List(req api.Context) error {

var resp types.AgentList
for _, agent := range agentList.Items {
resp.Items = append(resp.Items, *convertAgent(agent, api.GetURLPrefix(req)))
resp.Items = append(resp.Items, *convertAgent(agent, server.GetURLPrefix(req)))
}

return req.Write(resp)
Expand Down
9 changes: 5 additions & 4 deletions pkg/api/handlers/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/otto8-ai/otto8/apiclient/types"
"github.com/otto8-ai/otto8/pkg/api"
"github.com/otto8-ai/otto8/pkg/api/server"
v1 "github.com/otto8-ai/otto8/pkg/storage/apis/otto.gptscript.ai/v1"
"github.com/otto8-ai/otto8/pkg/system"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -53,7 +54,7 @@ func (a *WebhookHandler) Update(req api.Context) error {
return err
}

return req.Write(convertWebhook(wh, api.GetURLPrefix(req)))
return req.Write(convertWebhook(wh, server.GetURLPrefix(req)))
}

func (a *WebhookHandler) Delete(req api.Context) error {
Expand Down Expand Up @@ -98,7 +99,7 @@ func (a *WebhookHandler) Create(req api.Context) error {
}

req.WriteHeader(http.StatusCreated)
return req.Write(convertWebhook(wh, api.GetURLPrefix(req)))
return req.Write(convertWebhook(wh, server.GetURLPrefix(req)))
}

func convertWebhook(webhook v1.Webhook, urlPrefix string) *types.Webhook {
Expand All @@ -125,7 +126,7 @@ func (a *WebhookHandler) ByID(req api.Context) error {
return err
}

return req.Write(convertWebhook(wh, api.GetURLPrefix(req)))
return req.Write(convertWebhook(wh, server.GetURLPrefix(req)))
}

func (a *WebhookHandler) List(req api.Context) error {
Expand All @@ -136,7 +137,7 @@ func (a *WebhookHandler) List(req api.Context) error {

var resp types.WebhookList
for _, wh := range webhookList.Items {
resp.Items = append(resp.Items, *convertWebhook(wh, api.GetURLPrefix(req)))
resp.Items = append(resp.Items, *convertWebhook(wh, server.GetURLPrefix(req)))
}

return req.Write(resp)
Expand Down
9 changes: 5 additions & 4 deletions pkg/api/handlers/workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/gptscript-ai/go-gptscript"
"github.com/otto8-ai/otto8/apiclient/types"
"github.com/otto8-ai/otto8/pkg/api"
"github.com/otto8-ai/otto8/pkg/api/server"
"github.com/otto8-ai/otto8/pkg/controller/handlers/workflow"
"github.com/otto8-ai/otto8/pkg/render"
v1 "github.com/otto8-ai/otto8/pkg/storage/apis/otto.gptscript.ai/v1"
Expand Down Expand Up @@ -50,7 +51,7 @@ func (a *WorkflowHandler) Update(req api.Context) error {
return err
}

return req.Write(convertWorkflow(wf, api.GetURLPrefix(req)))
return req.Write(convertWorkflow(wf, server.GetURLPrefix(req)))
}

func (a *WorkflowHandler) Delete(req api.Context) error {
Expand Down Expand Up @@ -87,7 +88,7 @@ func (a *WorkflowHandler) Create(req api.Context) error {
}

req.WriteHeader(http.StatusCreated)
return req.Write(convertWorkflow(workflow, api.GetURLPrefix(req)))
return req.Write(convertWorkflow(workflow, server.GetURLPrefix(req)))
}

func convertWorkflow(workflow v1.Workflow, prefix string) *types.Workflow {
Expand All @@ -112,7 +113,7 @@ func (a *WorkflowHandler) ByID(req api.Context) error {
return err
}

return req.Write(convertWorkflow(workflow, api.GetURLPrefix(req)))
return req.Write(convertWorkflow(workflow, server.GetURLPrefix(req)))
}

func (a *WorkflowHandler) List(req api.Context) error {
Expand All @@ -123,7 +124,7 @@ func (a *WorkflowHandler) List(req api.Context) error {

var resp types.WorkflowList
for _, workflow := range workflowList.Items {
resp.Items = append(resp.Items, *convertWorkflow(workflow, api.GetURLPrefix(req)))
resp.Items = append(resp.Items, *convertWorkflow(workflow, server.GetURLPrefix(req)))
}

return req.Write(resp)
Expand Down
13 changes: 9 additions & 4 deletions pkg/api/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
"strings"
"time"

"github.com/acorn-io/baaah/pkg/router"
"github.com/gptscript-ai/go-gptscript"
"github.com/otto8-ai/otto8/apiclient/types"
"github.com/otto8-ai/otto8/pkg/api/authz"
"github.com/otto8-ai/otto8/pkg/storage"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
Expand All @@ -31,6 +31,11 @@ type Context struct {
User user.Info
}

type (
HandlerFunc func(Context) error
Middleware func(HandlerFunc) HandlerFunc
)

func (r *Context) IsStreamRequested() bool {
return r.Accepts("text/event-stream")
}
Expand Down Expand Up @@ -195,7 +200,7 @@ func (r *Context) Delete(obj client.Object) error {

func (r *Context) Get(obj client.Object, name string) error {
namespace := r.Namespace()
err := r.Storage.Get(r.Request.Context(), router.Key(namespace, name), obj)
err := r.Storage.Get(r.Request.Context(), client.ObjectKey{Namespace: namespace, Name: name}, obj)
if apierrors.IsNotFound(err) {
gvk, _ := r.Storage.GroupVersionKindFor(obj)
return types.NewErrHttp(http.StatusNotFound, fmt.Sprintf("%s %s not found", strings.ToLower(gvk.Kind), name))
Expand All @@ -216,11 +221,11 @@ func (r *Context) Namespace() string {
}

func (r *Context) UserIsAdmin() bool {
return slices.Contains(r.User.GetGroups(), "admin")
return slices.Contains(r.User.GetGroups(), authz.AdminGroup)
}

func (r *Context) UserIsAuthenticated() bool {
return slices.Contains(r.User.GetGroups(), "system:authenticated")
return slices.Contains(r.User.GetGroups(), authz.AuthenticatedGroup)
}

func (r *Context) UserID() uint {
Expand Down
21 changes: 21 additions & 0 deletions pkg/api/router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package api

import "net/http"

type Router interface {
HandleFunc(string, HandlerFunc, ...string)
HTTPHandle(string, http.Handler)
}

type router struct {
authorizedGroups []string
next Router
}

func (r *router) HandleFunc(pattern string, f HandlerFunc, authorizedGroups ...string) {
r.next.HandleFunc(pattern, f, append(r.authorizedGroups, authorizedGroups...)...)
}

func (r *router) HTTPHandle(pattern string, f http.Handler) {
r.next.HTTPHandle(pattern, f)
}
Loading

0 comments on commit 17c98eb

Please sign in to comment.