-
Notifications
You must be signed in to change notification settings - Fork 86
[WIP] permissions API #2246
base: master
Are you sure you want to change the base?
[WIP] permissions API #2246
Changes from 74 commits
b6fcaba
36e590a
58342c7
a45161a
dbcf947
99d3f01
e45ac78
2b06816
94159dc
9bb38c8
ad3b87f
e32a323
1a9481d
ff47976
29461ed
15ee913
7df6871
f9a252e
99f3d57
8193a2f
7ebe5eb
d1514f0
d2eb5e3
63c3ec9
fa0d3bd
2418c94
7a7c9e8
e4c3a6c
e2b0461
38e5982
270268d
58929c0
8d58119
3d9ebdb
57dd758
d8c553e
bd04f78
a768678
5136927
9fd101e
2c42ebc
e571140
efe93cd
0d756c5
d3325c0
2ae089a
271c627
34c95bb
39776d0
23c654e
4bfae3b
e5dd365
afae2f4
989e036
92d1a87
9e62c5c
ec4bad9
f28fce8
b814a8a
adbb22c
8ea65cb
9d34f9f
9b5b770
ad29c04
86eaae2
8375f46
8231adf
d3fe4c4
c904053
03e3aa0
ed803fa
5f64cc5
85236ea
421c6bd
6e601e8
bfae026
1f459ca
cb9fdb5
3fe9177
a6aeb73
aa8b1b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,13 +9,16 @@ import ( | |
"time" | ||
|
||
"github.com/fabric8-services/fabric8-wit/app" | ||
witclient "github.com/fabric8-services/fabric8-wit/client" | ||
"github.com/fabric8-services/fabric8-wit/configuration" | ||
"github.com/fabric8-services/fabric8-wit/errors" | ||
"github.com/fabric8-services/fabric8-wit/jsonapi" | ||
"github.com/fabric8-services/fabric8-wit/kubernetes" | ||
"github.com/fabric8-services/fabric8-wit/log" | ||
"github.com/fabric8-services/fabric8-wit/rest" | ||
|
||
"github.com/goadesign/goa" | ||
goauuid "github.com/goadesign/goa/uuid" | ||
errs "github.com/pkg/errors" | ||
uuid "github.com/satori/go.uuid" | ||
"golang.org/x/net/websocket" | ||
|
@@ -332,6 +335,9 @@ func (c *DeploymentsController) ShowDeploymentStats(ctx *app.ShowDeploymentStats | |
// ShowSpace runs the showSpace action. | ||
func (c *DeploymentsController) ShowSpace(ctx *app.ShowSpaceDeploymentsContext) error { | ||
|
||
// should we ask k8s for permissions on select objects? default is no | ||
checkPerms := ctx.Qp != nil && *ctx.Qp | ||
|
||
kc, err := c.GetKubeClient(ctx) | ||
defer cleanup(kc) | ||
if err != nil { | ||
|
@@ -355,6 +361,69 @@ func (c *DeploymentsController) ShowSpace(ctx *app.ShowSpaceDeploymentsContext) | |
// Kubernetes doesn't know about space ID, so add it here | ||
space.ID = ctx.SpaceID | ||
|
||
if checkPerms { | ||
// next, try to get permissions | ||
guid := goauuid.UUID(ctx.SpaceID) | ||
|
||
// permission for all deployments | ||
for _, appl := range space.Attributes.Applications { | ||
for _, depl := range appl.Attributes.Deployments { | ||
deployPath := witclient.SetDeploymentDeploymentsPath(guid, appl.Attributes.Name, depl.Attributes.Name) | ||
deployURLStr := rest.AbsoluteURL(ctx.Request, deployPath) | ||
if err != nil { | ||
return jsonapi.JSONErrorResponse(ctx, errs.Wrapf(err, "could not retrieve path for %s", *kubeSpaceName)) | ||
} | ||
envName := depl.Attributes.Name | ||
methods := []string{} | ||
canScale, err := kc.CanScaleDeployment(envName) | ||
if err != nil { | ||
return jsonapi.JSONErrorResponse(ctx, errs.Wrapf(err, "could not retrieve permission for %s", *kubeSpaceName)) | ||
} | ||
if canScale { | ||
methods = append(methods, "PATCH") | ||
} | ||
canDelete, err := kc.CanDeleteDeployment(envName) | ||
if err != nil { | ||
return jsonapi.JSONErrorResponse(ctx, errs.Wrapf(err, "could not retrieve permission for %s", *kubeSpaceName)) | ||
} | ||
if canDelete { | ||
methods = append(methods, "DELETE") | ||
} | ||
// NOTE: we don't actually allow GET to this endpoint, so don't claim we do by adding "GET" to methods | ||
depl.Links.Self = &app.LinkWithAccess{ | ||
Href: &deployURLStr, | ||
Meta: &app.EndpointAccess{ | ||
Methods: methods, | ||
}, | ||
} | ||
} | ||
} | ||
|
||
// permssions for this space | ||
spaceURLStr := rest.AbsoluteURL(ctx.Request, witclient.ShowSpacePath(guid)) | ||
if err != nil { | ||
return jsonapi.JSONErrorResponse(ctx, errs.Wrapf(err, "could not retrieve path for %s", *kubeSpaceName)) | ||
} | ||
|
||
methods := []string{} | ||
canGetSpace, err := kc.CanGetSpace() | ||
if err != nil { | ||
return jsonapi.JSONErrorResponse(ctx, errs.Wrapf(err, "could not retrieve permission for %s", *kubeSpaceName)) | ||
} | ||
if canGetSpace { | ||
methods = append(methods, "GET") | ||
} | ||
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. CanGetSpace returns whether we can GET 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 figured it was moot since they have to to the GET to get there in the first place, and GET is all we allow. But I didn't realize at the time the permissions are different. I have removed this link. |
||
// DELETE, etc come later | ||
space.Links = &app.SimpleSpaceLinks{ | ||
Space: &app.LinkWithAccess{ | ||
Href: &spaceURLStr, | ||
Meta: &app.EndpointAccess{ | ||
Methods: methods, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
res := &app.SimpleSpaceSingle{ | ||
Data: space, | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,7 +30,7 @@ import ( | |
) | ||
|
||
const ( | ||
// APIStringTypeCodebase contains the JSON API type for codebases | ||
// APIStringTypeSpace contains the JSON API type for codebases | ||
APIStringTypeSpace = "spaces" | ||
) | ||
|
||
|
@@ -48,13 +48,14 @@ type SpaceController struct { | |
resourceManager auth.ResourceManager | ||
DeploymentsClient *http.Client | ||
CodebaseClient *http.Client | ||
ClientGetter | ||
} | ||
|
||
// NewSpaceController creates a space controller. | ||
func NewSpaceController( | ||
service *goa.Service, | ||
db application.DB, | ||
config SpaceConfiguration, | ||
config *configuration.Registry, | ||
resourceManager auth.ResourceManager) *SpaceController { | ||
|
||
return &SpaceController{ | ||
|
@@ -64,6 +65,9 @@ func NewSpaceController( | |
resourceManager: resourceManager, | ||
DeploymentsClient: http.DefaultClient, | ||
CodebaseClient: http.DefaultClient, | ||
ClientGetter: &defaultClientGetter{ | ||
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 DeleteSpace method uses the above DeploymentsClient to call the deployments API via the Goa-generated client. Should we do the same here instead of using the ClientGetter? 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 did it this way because the new CanGet*() API's are internal, and it seemed more efficient to call directly. However, it's messy in several ways: I had to pass in config.Registry instead of SpaceConfiguration, and it uses the internal CanGetSpace() call, which is ... internal. So I felt slightly dirty coding it. If I understand it, your proposal is to call //deployments/Space/{spaceID}?qp=true under the hood and use the output of that? 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 right, I forgot that in order to get the permissions data we have to get all the space data as well. I don't think this would be a good idea, because of the substantial overhead for data we won't use. Perhaps we should stick with your current approach. 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. We could add a parameter to only get the root space object, but maybe I should just leave that in a comment? 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. Or even just skip getting any actual data for the space and only return the permissions. This may be over-complicating one API endpoint though. I'm fine with leaving as a comment for now. I imagine this would need to be addressed when moving deployments out of WIT though. |
||
config: config, | ||
}, | ||
} | ||
} | ||
|
||
|
@@ -364,7 +368,7 @@ func deleteOpenShiftResource( | |
|
||
// get all the apps and envs | ||
path := client.ShowSpaceDeploymentsPath(spaceID) | ||
resp, err := cl.ShowSpaceDeployments(ctx, path) | ||
resp, err := cl.ShowSpaceDeployments(ctx, path, nil) | ||
if err != nil { | ||
return errors.NewInternalError(ctx, | ||
fmt.Errorf("could not get deployments: %v", err)) | ||
|
@@ -475,6 +479,7 @@ func (c *SpaceController) List(ctx *app.ListSpaceContext) error { | |
|
||
// Show runs the show action. | ||
func (c *SpaceController) Show(ctx *app.ShowSpaceContext) error { | ||
|
||
var s *space.Space | ||
err := application.Transactional(c.db, func(appl application.Application) error { | ||
var err error | ||
|
@@ -490,8 +495,46 @@ func (c *SpaceController) Show(ctx *app.ShowSpaceContext) error { | |
if err != nil { | ||
return jsonapi.JSONErrorResponse(ctx, err) | ||
} | ||
|
||
var methods []string | ||
if ctx.Qp != nil && *ctx.Qp { | ||
// 'qp' (QueryPerms query parameter) is true: | ||
// ask Kubernetes if this user can access this space | ||
// (this is different from the WIT Space database access) | ||
kc, err := c.GetKubeClient(ctx) | ||
defer cleanup(kc) | ||
if err != nil { | ||
log.Error(ctx, map[string]interface{}{ | ||
"err": err, | ||
"space_id": ctx.SpaceID, | ||
}, "unable create a KubeClient") | ||
return jsonapi.JSONErrorResponse(ctx, err) | ||
} | ||
canRead, err := kc.CanGetSpace() | ||
if err != nil { | ||
log.Error(ctx, map[string]interface{}{ | ||
"err": err, | ||
"space_id": ctx.SpaceID, | ||
}, "unable retrieve permissions from CanGetSpace()") | ||
return jsonapi.JSONErrorResponse(ctx, err) | ||
} | ||
if canRead { | ||
methods = []string{"GET"} | ||
} else { | ||
methods = []string{} | ||
} | ||
} else { | ||
methods = []string{} | ||
} | ||
|
||
return ctx.ConditionalRequest(*s, c.config.GetCacheControlSpace, func() error { | ||
spaceData, err := ConvertSpaceFromModel(ctx.Request, *s, IncludeBacklogTotalCount(ctx.Context, c.db)) | ||
var spaceData *app.Space | ||
var err error | ||
if ctx.Qp != nil && *ctx.Qp { | ||
spaceData, err = ConvertSpaceFromModel(ctx.Request, *s, IncludeBacklogTotalCount(ctx.Context, c.db), IncludeMethodAccess(ctx.Context, ctx.Request, methods)) | ||
} else { | ||
spaceData, err = ConvertSpaceFromModel(ctx.Request, *s, IncludeBacklogTotalCount(ctx.Context, c.db)) | ||
} | ||
if err != nil { | ||
log.Error(ctx, map[string]interface{}{ | ||
"err": err, | ||
|
@@ -632,7 +675,7 @@ func ConvertSpaceToModel(appSpace app.Space) space.Space { | |
// conversion from internal to API | ||
type SpaceConvertFunc func(*http.Request, *space.Space, *app.Space) error | ||
|
||
// IncludeBacklog returns a SpaceConvertFunc that includes the a link to the backlog | ||
// IncludeBacklogTotalCount returns a SpaceConvertFunc that includes the a link to the backlog | ||
// along with the total count of items in the backlog of the current space | ||
func IncludeBacklogTotalCount(ctx context.Context, db application.DB) SpaceConvertFunc { | ||
return func(req *http.Request, modelSpace *space.Space, appSpace *app.Space) error { | ||
|
@@ -646,6 +689,22 @@ func IncludeBacklogTotalCount(ctx context.Context, db application.DB) SpaceConve | |
} | ||
} | ||
|
||
// IncludeMethodAccess returns a SpaceConvertFunc that includes method access metadata | ||
// along with the total count of items in the backlog of the current space | ||
func IncludeMethodAccess(ctx context.Context, req *http.Request, perms []string) SpaceConvertFunc { | ||
return func(req *http.Request, modelSpace *space.Space, appSpace *app.Space) error { | ||
spaceIDStr := modelSpace.ID.String() | ||
relatedDeployments := rest.AbsoluteURL(req, fmt.Sprintf("/api/deployments/spaces/%s", spaceIDStr)) | ||
appSpace.Links.Deployments = &app.LinkWithAccess{ | ||
Href: &relatedDeployments, | ||
Meta: &app.EndpointAccess{ | ||
Methods: perms, | ||
}, | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
// ConvertSpacesFromModel converts between internal and external REST representation | ||
func ConvertSpacesFromModel(request *http.Request, spaces []space.Space, additional ...SpaceConvertFunc) ([]*app.Space, error) { | ||
var result = make([]*app.Space, len(spaces)) | ||
|
@@ -662,6 +721,12 @@ func ConvertSpacesFromModel(request *http.Request, spaces []space.Space, additio | |
// ConvertSpaceFromModel converts between internal and external REST representation | ||
func ConvertSpaceFromModel(request *http.Request, sp space.Space, options ...SpaceConvertFunc) (*app.Space, error) { | ||
selfURL := rest.AbsoluteURL(request, app.SpaceHref(sp.ID)) | ||
/** selfWithPerms := &app.LinkWithAccess{ | ||
Href: &selfURL, | ||
Meta: &app.EndpointAccess{ | ||
Methods: []string{"GET", "POST", "DELETE", "PATCH"}, | ||
}, | ||
}**/ | ||
spaceIDStr := sp.ID.String() | ||
relatedIterations := rest.AbsoluteURL(request, fmt.Sprintf("/api/spaces/%s/iterations", spaceIDStr)) | ||
relatedAreas := rest.AbsoluteURL(request, fmt.Sprintf("/api/spaces/%s/areas", spaceIDStr)) | ||
|
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.
We could also include links to the
stats
andstatseries
endpoints here.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.
Good idea - done.