-
Notifications
You must be signed in to change notification settings - Fork 881
feat: Rbac more coderd endpoints, unit test to confirm #1406
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
Changes from 1 commit
bed0f8f
af6dc5f
01b2c94
be5b0b3
970e345
945e9fa
fdfef88
89a3678
29da9aa
63727e0
96a5727
4b6c9b0
cd2fda7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Make 'request()' exported for running custom requests
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,8 @@ import ( | |
"testing" | ||
"time" | ||
|
||
"github.com/coder/coder/coderd/rbac" | ||
|
||
"cloud.google.com/go/compute/metadata" | ||
"github.com/fullsailor/pkcs7" | ||
"github.com/golang-jwt/jwt" | ||
|
@@ -57,11 +59,10 @@ type Options struct { | |
GoogleTokenValidator *idtoken.Validator | ||
SSHKeygenAlgorithm gitsshkey.Algorithm | ||
APIRateLimit int | ||
Authorizer rbac.Authorizer | ||
} | ||
|
||
// New constructs an in-memory coderd instance and returns | ||
// the connected client. | ||
func New(t *testing.T, options *Options) *codersdk.Client { | ||
func NewMemoryCoderd(t *testing.T, options *Options) (*httptest.Server, *codersdk.Client) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd tried something before where I returned a Also, having control of the test server is definitely something we want to have available in unit tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, I really didn't want to touch much. I just needed access to the server. If you pass things into the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because the test consuming this is just using the HTTP handler returned and not the actual server, it might be clearer to just call Considering none of our other tests have required access to the HTTP server object, it seems like that could be cleaner. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The thing is, if I don't use the I will need to make a template & workspace for certain endpoints to not return 404's. It is much easier to know the coderd I am interacting with works when I use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only thing it should need to check for permissions is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but the unit test calls all possible routes, and checks if I could do this just on the handler returned by So I could probably make it work without calling The only downside is that I need to expose the server to the unit test to grab the handler to walk it. |
||
if options == nil { | ||
options = &Options{} | ||
} | ||
|
@@ -129,6 +130,7 @@ func New(t *testing.T, options *Options) *codersdk.Client { | |
SSHKeygenAlgorithm: options.SSHKeygenAlgorithm, | ||
TURNServer: turnServer, | ||
APIRateLimit: options.APIRateLimit, | ||
Authorizer: options.Authorizer, | ||
}) | ||
t.Cleanup(func() { | ||
cancelFunc() | ||
|
@@ -137,7 +139,14 @@ func New(t *testing.T, options *Options) *codersdk.Client { | |
closeWait() | ||
}) | ||
|
||
return codersdk.New(serverURL) | ||
return srv, codersdk.New(serverURL) | ||
} | ||
|
||
// New constructs an in-memory coderd instance and returns | ||
// the connected client. | ||
func New(t *testing.T, options *Options) *codersdk.Client { | ||
_, cli := NewMemoryCoderd(t, options) | ||
return cli | ||
} | ||
|
||
// NewProvisionerDaemon launches a provisionerd instance configured to work | ||
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -4,6 +4,8 @@ import ( | |||
"context" | ||||
"net/http" | ||||
|
||||
"github.com/google/uuid" | ||||
|
||||
"golang.org/x/xerrors" | ||||
|
||||
"cdr.dev/slog" | ||||
|
@@ -15,11 +17,12 @@ import ( | |||
// Authorize will enforce if the user roles can complete the action on the AuthObject. | ||||
// The organization and owner are found using the ExtractOrganization and | ||||
// ExtractUser middleware if present. | ||||
func Authorize(logger slog.Logger, auth *rbac.RegoAuthorizer, action rbac.Action) func(http.Handler) http.Handler { | ||||
func Authorize(logger slog.Logger, auth rbac.Authorizer, action rbac.Action) func(http.Handler) http.Handler { | ||||
return func(next http.Handler) http.Handler { | ||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||||
roles := UserRoles(r) | ||||
object := rbacObject(r) | ||||
authObject := rbacObject(r) | ||||
object := authObject.Object | ||||
|
||||
if object.Type == "" { | ||||
panic("developer error: auth object has no type") | ||||
|
@@ -34,12 +37,18 @@ func Authorize(logger slog.Logger, auth *rbac.RegoAuthorizer, action rbac.Action | |||
object = object.InOrg(organization.ID) | ||||
} | ||||
|
||||
unknownOwner := r.Context().Value(userParamContextKey{}) | ||||
if owner, castOK := unknownOwner.(database.User); unknownOwner != nil { | ||||
if !castOK { | ||||
panic("developer error: user param middleware not provided for authorize") | ||||
if authObject.WithOwner != nil { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This design seems to leave us in a place where we're calling Authorize before we've actually constructed the full object, so the first part of this method is collecting bits of information about the object itself. Is there a way that we can construct the object first, then call authorize? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think I follow this comment. This
The actual auth call is done at the bottom: coder/coderd/httpmw/authorize.go Line 54 in 96a5727
|
||||
owner := authObject.WithOwner(r) | ||||
object = object.WithOwner(owner.String()) | ||||
} else { | ||||
// Attempt to find the resource owner id | ||||
unknownOwner := r.Context().Value(userParamContextKey{}) | ||||
if owner, castOK := unknownOwner.(database.User); unknownOwner != nil { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's very strange to me to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see how it is confusing, I can rework this logic flow |
||||
if !castOK { | ||||
panic("developer error: user param middleware not provided for authorize") | ||||
} | ||||
object = object.WithOwner(owner.ID.String()) | ||||
} | ||||
object = object.WithOwner(owner.ID.String()) | ||||
} | ||||
|
||||
err := auth.AuthorizeByRoleName(r.Context(), roles.ID.String(), roles.Roles, action, object) | ||||
|
@@ -70,21 +79,59 @@ func Authorize(logger slog.Logger, auth *rbac.RegoAuthorizer, action rbac.Action | |||
|
||||
type authObjectKey struct{} | ||||
|
||||
type AuthObject struct { | ||||
Emyrk marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
Object rbac.Object | ||||
|
||||
WithOwner func(r *http.Request) uuid.UUID | ||||
} | ||||
|
||||
// APIKey returns the API key from the ExtractAPIKey handler. | ||||
func rbacObject(r *http.Request) rbac.Object { | ||||
obj, ok := r.Context().Value(authObjectKey{}).(rbac.Object) | ||||
func rbacObject(r *http.Request) AuthObject { | ||||
obj, ok := r.Context().Value(authObjectKey{}).(AuthObject) | ||||
if !ok { | ||||
panic("developer error: auth object middleware not provided") | ||||
} | ||||
return obj | ||||
} | ||||
|
||||
func WithAPIKeyAsOwner() func(http.Handler) http.Handler { | ||||
return WithOwner(func(r *http.Request) uuid.UUID { | ||||
key := APIKey(r) | ||||
return key.UserID | ||||
}) | ||||
} | ||||
|
||||
// WithOwner sets the object owner for 'Authorize()' for all routes handled | ||||
// by this middleware. | ||||
func WithOwner(withOwner func(r *http.Request) uuid.UUID) func(http.Handler) http.Handler { | ||||
return func(next http.Handler) http.Handler { | ||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||||
obj, ok := r.Context().Value(authObjectKey{}).(AuthObject) | ||||
if ok { | ||||
obj.WithOwner = withOwner | ||||
} else { | ||||
obj = AuthObject{WithOwner: withOwner} | ||||
} | ||||
|
||||
ctx := context.WithValue(r.Context(), authObjectKey{}, obj) | ||||
next.ServeHTTP(rw, r.WithContext(ctx)) | ||||
}) | ||||
} | ||||
} | ||||
|
||||
// WithRBACObject sets the object for 'Authorize()' for all routes handled | ||||
// by this middleware. The important field to set is 'Type' | ||||
func WithRBACObject(object rbac.Object) func(http.Handler) http.Handler { | ||||
return func(next http.Handler) http.Handler { | ||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||||
ctx := context.WithValue(r.Context(), authObjectKey{}, object) | ||||
obj, ok := r.Context().Value(authObjectKey{}).(AuthObject) | ||||
if ok { | ||||
obj.Object = object | ||||
} else { | ||||
obj = AuthObject{Object: object} | ||||
} | ||||
|
||||
ctx := context.WithValue(r.Context(), authObjectKey{}, obj) | ||||
next.ServeHTTP(rw, r.WithContext(ctx)) | ||||
}) | ||||
} | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import ( | |
"context" | ||
"fmt" | ||
"net/http" | ||
"reflect" | ||
|
||
"golang.org/x/oauth2" | ||
|
||
|
@@ -46,7 +47,8 @@ func OAuth2(r *http.Request) OAuth2State { | |
func ExtractOAuth2(config OAuth2Config) func(http.Handler) http.Handler { | ||
return func(next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||
if config == nil { | ||
// Interfaces can hold a nil value | ||
if config == nil || reflect.ValueOf(config).IsNil() { | ||
Comment on lines
+50
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was a bug that was causing panics in unit tests.
Comment on lines
+50
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm confused why the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because of how we pass the nil value. We pass it from a struct, and Go is being Go. See this: It sucks, I was trying to rework it to fix it, and it's kinda a PIA There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One way we can fix this is avoid passing in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very interesting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. r.Route("/github", func(r chi.Router) {
if options.GithubOAuth2Config != nil {
r.Use(httpmw.ExtractOAuth2(options.GithubOAuth2Config))
r.Get("/callback", api.userOAuth2Github)
}
}) |
||
httpapi.Write(rw, http.StatusPreconditionRequired, httpapi.Response{ | ||
Message: "The oauth2 method requested is not configured!", | ||
}) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,9 +35,9 @@ type Client struct { | |
|
||
type requestOption func(*http.Request) | ||
|
||
// request performs an HTTP request with the body provided. | ||
// Request performs an HTTP request with the body provided. | ||
// The caller is responsible for closing the response body. | ||
func (c *Client) request(ctx context.Context, method, path string, body interface{}, opts ...requestOption) (*http.Response, error) { | ||
func (c *Client) Request(ctx context.Context, method, path string, body interface{}, opts ...requestOption) (*http.Response, error) { | ||
Comment on lines
+38
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had to export this to use it generically in my unit test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This brings sadness There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be helpful though as now we can make unit tests that send malformed payloads 🤷♂️ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not opposed to having both in package and out of package tests for different kinds of testing (black box/white box) |
||
serverURL, err := c.URL.Parse(path) | ||
if err != nil { | ||
return nil, xerrors.Errorf("parse url: %w", err) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
praise: This is a really useful check to have. Nice one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯