- Notifications
You must be signed in to change notification settings - Fork928
feat: Implement list roles & enforce authorize examples#1273
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
30e2031
2161f84
54bc054
d083a7c
95b9a14
1498dcd
f36ae37
b831260
db04d67
b76f373
117f838
42b42ab
0efe72c
dba617d
190940f
c86c67c
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -12,6 +12,7 @@ import ( | ||||||||||||||
"github.com/go-chi/chi/v5" | ||||||||||||||
"github.com/go-chi/chi/v5/middleware" | ||||||||||||||
"github.com/pion/webrtc/v3" | ||||||||||||||
"golang.org/x/xerrors" | ||||||||||||||
"google.golang.org/api/idtoken" | ||||||||||||||
chitrace"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-chi/chi.v5" | ||||||||||||||
@@ -23,6 +24,7 @@ import ( | ||||||||||||||
"github.com/coder/coder/coderd/gitsshkey" | ||||||||||||||
"github.com/coder/coder/coderd/httpapi" | ||||||||||||||
"github.com/coder/coder/coderd/httpmw" | ||||||||||||||
"github.com/coder/coder/coderd/rbac" | ||||||||||||||
"github.com/coder/coder/coderd/turnconn" | ||||||||||||||
"github.com/coder/coder/codersdk" | ||||||||||||||
"github.com/coder/coder/site" | ||||||||||||||
@@ -48,6 +50,7 @@ type Options struct { | ||||||||||||||
SecureAuthCookiebool | ||||||||||||||
SSHKeygenAlgorithm gitsshkey.Algorithm | ||||||||||||||
TURNServer*turnconn.Server | ||||||||||||||
Authorizer*rbac.RegoAuthorizer | ||||||||||||||
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. Does this need to be exposed via options? It doesn't seem to be used outside of here. 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 was thinking of making it an interface that is "denyall" or something to trigger in unit tests. But for now, we don't, so I'll drop it from 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. Ah wait yes.@kylecarbs this will be needed if you decide to do the rbac Member 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 could go on 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. Right now nothing else is on the api only like that. Lines 333 to 338 indb04d67
I don't think it'd be bad to pass one in via options 🤷♂️ 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. Oh my bad, I thought there was precedent for this 🤦 If you feel like it's fine I'll defer, but I generally think it's hasty to expose a value unless it needs to be used elsewhere. It's really easy to expose a parameter, but much harder to hide one. | ||||||||||||||
} | ||||||||||||||
// New constructs the Coder API into an HTTP handler. | ||||||||||||||
@@ -61,13 +64,29 @@ func New(options *Options) (http.Handler, func()) { | ||||||||||||||
ifoptions.APIRateLimit==0 { | ||||||||||||||
options.APIRateLimit=512 | ||||||||||||||
} | ||||||||||||||
ifoptions.Authorizer==nil { | ||||||||||||||
varerrerror | ||||||||||||||
options.Authorizer,err=rbac.NewAuthorizer() | ||||||||||||||
iferr!=nil { | ||||||||||||||
// This should never happen, as the unit tests would fail if the | ||||||||||||||
// default built in authorizer failed. | ||||||||||||||
panic(xerrors.Errorf("rego authorize panic: %w",err)) | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
api:=&api{ | ||||||||||||||
Options:options, | ||||||||||||||
} | ||||||||||||||
apiKeyMiddleware:=httpmw.ExtractAPIKey(options.Database,&httpmw.OAuth2Configs{ | ||||||||||||||
Github:options.GithubOAuth2Config, | ||||||||||||||
}) | ||||||||||||||
// TODO: @emyrk we should just move this into 'ExtractAPIKey'. | ||||||||||||||
authRolesMiddleware:=httpmw.ExtractUserRoles(options.Database) | ||||||||||||||
authorize:=func(f http.HandlerFunc,actions rbac.Action) http.HandlerFunc { | ||||||||||||||
returnhttpmw.Authorize(api.Logger,api.Authorizer,actions)(f).ServeHTTP | ||||||||||||||
} | ||||||||||||||
r:=chi.NewRouter() | ||||||||||||||
r.Use( | ||||||||||||||
@@ -119,6 +138,7 @@ func New(options *Options) (http.Handler, func()) { | ||||||||||||||
r.Use( | ||||||||||||||
apiKeyMiddleware, | ||||||||||||||
httpmw.ExtractOrganizationParam(options.Database), | ||||||||||||||
authRolesMiddleware, | ||||||||||||||
) | ||||||||||||||
r.Get("/",api.organization) | ||||||||||||||
r.Get("/provisionerdaemons",api.provisionerDaemonsByOrganization) | ||||||||||||||
@@ -138,6 +158,10 @@ func New(options *Options) (http.Handler, func()) { | ||||||||||||||
}) | ||||||||||||||
}) | ||||||||||||||
r.Route("/members",func(r chi.Router) { | ||||||||||||||
r.Route("/roles",func(r chi.Router) { | ||||||||||||||
r.Use(httpmw.WithRBACObject(rbac.ResourceUserRole)) | ||||||||||||||
r.Get("/",authorize(api.assignableOrgRoles,rbac.ActionRead)) | ||||||||||||||
}) | ||||||||||||||
r.Route("/{user}",func(r chi.Router) { | ||||||||||||||
r.Use( | ||||||||||||||
httpmw.ExtractUserParam(options.Database), | ||||||||||||||
@@ -200,20 +224,28 @@ func New(options *Options) (http.Handler, func()) { | ||||||||||||||
}) | ||||||||||||||
}) | ||||||||||||||
r.Group(func(r chi.Router) { | ||||||||||||||
r.Use( | ||||||||||||||
apiKeyMiddleware, | ||||||||||||||
authRolesMiddleware, | ||||||||||||||
) | ||||||||||||||
r.Post("/",api.postUser) | ||||||||||||||
r.Get("/",api.users) | ||||||||||||||
// These routes query information about site wide roles. | ||||||||||||||
r.Route("/roles",func(r chi.Router) { | ||||||||||||||
r.Use(httpmw.WithRBACObject(rbac.ResourceUserRole)) | ||||||||||||||
r.Get("/",authorize(api.assignableSiteRoles,rbac.ActionRead)) | ||||||||||||||
}) | ||||||||||||||
r.Route("/{user}",func(r chi.Router) { | ||||||||||||||
r.Use(httpmw.ExtractUserParam(options.Database)) | ||||||||||||||
r.Get("/",api.userByName) | ||||||||||||||
r.Put("/profile",api.putUserProfile) | ||||||||||||||
r.Put("/suspend",api.putUserSuspend) | ||||||||||||||
r.Get("/organizations",api.organizationsByUser) | ||||||||||||||
r.Post("/organizations",api.postOrganizationsByUser) | ||||||||||||||
// These roles apply to the site wide permissions. | ||||||||||||||
r.Put("/roles",api.putUserRoles) | ||||||||||||||
r.Get("/roles",api.userRoles) | ||||||||||||||
r.Post("/keys",api.postAPIKey) | ||||||||||||||
r.Route("/organizations",func(r chi.Router) { | ||||||||||||||
r.Post("/",api.postOrganizationsByUser) | ||||||||||||||
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package httpmw | ||
import ( | ||
"context" | ||
"net/http" | ||
"golang.org/x/xerrors" | ||
"cdr.dev/slog" | ||
"github.com/coder/coder/coderd/database" | ||
"github.com/coder/coder/coderd/httpapi" | ||
"github.com/coder/coder/coderd/rbac" | ||
) | ||
// 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. | ||
funcAuthorize(logger slog.Logger,auth*rbac.RegoAuthorizer,action rbac.Action)func(http.Handler) http.Handler { | ||
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.
What do you think about renaming this to 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 think
Another word that comes to mind is "Access". Idk, 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. Fair enough. I'm primarily trying to display that the While it isauthorizing, I'm nervous that this will get conflated with authentication really easily. 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 this is classic authorization vs authentication. If you aren't familiar with it, it's easy to mix up. 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. Agreed agreed | ||
returnfunc(next http.Handler) http.Handler { | ||
returnhttp.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) { | ||
roles:=UserRoles(r) | ||
object:=rbacObject(r) | ||
ifobject.Type=="" { | ||
panic("developer error: auth object has no type") | ||
} | ||
// First extract the object's owner and organization if present. | ||
unknownOrg:=r.Context().Value(organizationParamContextKey{}) | ||
iforganization,castOK:=unknownOrg.(database.Organization);unknownOrg!=nil { | ||
if!castOK { | ||
panic("developer error: organization param middleware not provided for authorize") | ||
} | ||
object=object.InOrg(organization.ID) | ||
} | ||
unknownOwner:=r.Context().Value(userParamContextKey{}) | ||
ifowner,castOK:=unknownOwner.(database.User);unknownOwner!=nil { | ||
if!castOK { | ||
panic("developer error: user param middleware not provided for authorize") | ||
} | ||
object=object.WithOwner(owner.ID.String()) | ||
} | ||
err:=auth.AuthorizeByRoleName(r.Context(),roles.ID.String(),roles.Roles,action,object) | ||
iferr!=nil { | ||
internalError:=new(rbac.UnauthorizedError) | ||
ifxerrors.As(err,internalError) { | ||
logger=logger.With(slog.F("internal",internalError.Internal())) | ||
} | ||
// Log information for debugging. This will be very helpful | ||
// in the early days if we over secure endpoints. | ||
logger.Warn(r.Context(),"unauthorized", | ||
slog.F("roles",roles.Roles), | ||
slog.F("user_id",roles.ID), | ||
slog.F("username",roles.Username), | ||
slog.F("route",r.URL.Path), | ||
slog.F("action",action), | ||
slog.F("object",object), | ||
) | ||
httpapi.Write(rw,http.StatusUnauthorized, httpapi.Response{ | ||
Message:err.Error(), | ||
}) | ||
return | ||
} | ||
next.ServeHTTP(rw,r) | ||
}) | ||
} | ||
} | ||
typeauthObjectKeystruct{} | ||
// APIKey returns the API key from the ExtractAPIKey handler. | ||
funcrbacObject(r*http.Request) rbac.Object { | ||
obj,ok:=r.Context().Value(authObjectKey{}).(rbac.Object) | ||
if!ok { | ||
panic("developer error: auth object middleware not provided") | ||
} | ||
returnobj | ||
} | ||
// WithRBACObject sets the object for 'Authorize()' for all routes handled | ||
// by this middleware. The important field to set is 'Type' | ||
funcWithRBACObject(object rbac.Object)func(http.Handler) http.Handler { | ||
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 might be confusing that this is called | ||
returnfunc(next http.Handler) http.Handler { | ||
returnhttp.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) { | ||
ctx:=context.WithValue(r.Context(),authObjectKey{},object) | ||
next.ServeHTTP(rw,r.WithContext(ctx)) | ||
}) | ||
} | ||
} | ||
// User roles are the 'subject' field of Authorize() | ||
typeuserRolesKeystruct{} | ||
// UserRoles returns the API key from the ExtractUserRoles handler. | ||
funcUserRoles(r*http.Request) database.GetAllUserRolesRow { | ||
apiKey,ok:=r.Context().Value(userRolesKey{}).(database.GetAllUserRolesRow) | ||
if!ok { | ||
panic("developer error: user roles middleware not provided") | ||
} | ||
returnapiKey | ||
} | ||
// ExtractUserRoles requires authentication using a valid API key. | ||
funcExtractUserRoles(db database.Store)func(http.Handler) http.Handler { | ||
returnfunc(next http.Handler) http.Handler { | ||
returnhttp.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) { | ||
apiKey:=APIKey(r) | ||
role,err:=db.GetAllUserRoles(r.Context(),apiKey.UserID) | ||
iferr!=nil { | ||
httpapi.Write(rw,http.StatusUnauthorized, httpapi.Response{ | ||
Message:"roles not found", | ||
}) | ||
return | ||
} | ||
ctx:=context.WithValue(r.Context(),userRolesKey{},role) | ||
next.ServeHTTP(rw,r.WithContext(ctx)) | ||
}) | ||
} | ||
} |
Uh oh!
There was an error while loading.Please reload this page.