pax_global_header00006660000000000000000000000064140556460030014515gustar00rootroot0000000000000052 comment=049a12d268b7816348d379ffbbd11e0a1091263c aclstore-2.1.0/000077500000000000000000000000001405564600300133315ustar00rootroot00000000000000aclstore-2.1.0/.travis.yml000066400000000000000000000004311405564600300154400ustar00rootroot00000000000000language: go go_import_path: "github.com/juju/aclstore" go: - "1.15" - "1.16" before_install: - "go get github.com/rogpeppe/godeps" install: - "go get -d github.com/juju/aclstore" - "godeps -u $GOPATH/src/github.com/juju/aclstore/dependencies.tsv" script: go test ./... aclstore-2.1.0/LICENSE000066400000000000000000000215011405564600300143350ustar00rootroot00000000000000All files in this repository are licensed as follows. If you contribute to this repository, it is assumed that you license your contribution under the same license unless you state otherwise. All files Copyright (C) 2018 Canonical Ltd. unless otherwise specified in the file. This software is licensed under the LGPLv3, included below. As a special exception to the GNU Lesser General Public License version 3 ("LGPL3"), the copyright holders of this Library give you permission to convey to a third party a Combined Work that links statically or dynamically to this Library without providing any Minimal Corresponding Source or Minimal Application Code as set out in 4d or providing the installation information set out in section 4e, provided that you comply with the other provisions of LGPL3 and provided that you meet, for the Application the terms and conditions of the license(s) which apply to the Application. Except as stated in this special exception, the provisions of LGPL3 will continue to comply in full to this Library. If you modify this Library, you may apply this exception to your version of this Library, but you are not obliged to do so. If you do not wish to do so, delete this exception statement from your version. This exception does not (and cannot) modify any license terms which apply to the Application, with which you must still comply. GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. aclstore-2.1.0/README.md000066400000000000000000000002041405564600300146040ustar00rootroot00000000000000aclstore: a persistent store for ACLs This makes it easy for a service to add role-based access controls for a fixed set of roles. aclstore-2.1.0/aclclient/000077500000000000000000000000001405564600300152675ustar00rootroot00000000000000aclstore-2.1.0/aclclient/client.go000066400000000000000000000041301405564600300170720ustar00rootroot00000000000000package aclclient import ( "context" errgo "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/juju/aclstore/v2/params" ) //go:generate httprequest-generate-client github.com/juju/aclstore/v2 handler1 client // Client represents an ACL store client. type Client struct { client } // NewParams holds the parameters for creating a new client. type NewParams struct { // BaseURL holds the URL prefix of all the endpoints in the ACL store. BaseURL string // Doer is used to make HTTP requests to the ACL store. Doer httprequest.Doer } // New returns a new client. func New(p NewParams) *Client { var c Client c.Client.BaseURL = p.BaseURL c.Client.Doer = p.Doer return &c } // Get retrieves the contents of the given ACL. func (c *Client) Get(ctx context.Context, name string) ([]string, error) { resp, err := c.GetACL(ctx, ¶ms.GetACLRequest{ Name: name, }) if err != nil { return nil, errgo.Mask(err, isRemoteError) } return resp.Users, nil } // Set updates the contents of the given ACL to the given user list. func (c *Client) Set(ctx context.Context, name string, users []string) error { err := c.SetACL(ctx, ¶ms.SetACLRequest{ Name: name, Body: params.SetACLRequestBody{ Users: users, }, }) return errgo.Mask(err, isRemoteError) } // Add updates the contents of the given ACL to include the given user // list. func (c *Client) Add(ctx context.Context, name string, users []string) error { err := c.ModifyACL(ctx, ¶ms.ModifyACLRequest{ Name: name, Body: params.ModifyACLRequestBody{ Add: users, }, }) return errgo.Mask(err, isRemoteError) } // Remove updates the contents of the given ACL to remove those in the // given user list. func (c *Client) Remove(ctx context.Context, name string, users []string) error { err := c.ModifyACL(ctx, ¶ms.ModifyACLRequest{ Name: name, Body: params.ModifyACLRequestBody{ Remove: users, }, }) return errgo.Mask(err, isRemoteError) } // isRemoteError determines whether the given error is a // httprequest.RemoteError. func isRemoteError(err error) bool { _, ok := err.(*httprequest.RemoteError) return ok } aclstore-2.1.0/aclclient/client_generated.go000066400000000000000000000030351405564600300211130ustar00rootroot00000000000000// The code in this file was automatically generated by running httprequest-generate-client. // DO NOT EDIT package aclclient import ( "context" "github.com/juju/aclstore/v2/params" "gopkg.in/httprequest.v1" ) type client struct { Client httprequest.Client } // GetACL returns the members of the ACL with the requested name. // Only administrators and members of the meta-ACL for the name // may access this endpoint. The meta-ACL for meta-ACLs is "admin". func (c *client) GetACL(ctx context.Context, p *params.GetACLRequest) (*params.GetACLResponse, error) { var r *params.GetACLResponse err := c.Client.Call(ctx, p, &r) return r, err } // GetACLs returns the list of all ACLs. // Only administrators may access this endpoint. func (c *client) GetACLs(ctx context.Context, p *params.GetACLsRequest) (*params.GetACLsResponse, error) { var r *params.GetACLsResponse err := c.Client.Call(ctx, p, &r) return r, err } // ModifyACL modifies the members of the ACL with the requested name. // Only administrators and members of the meta-ACL for the name // may access this endpoint. The meta-ACL for meta-ACLs is "admin". func (c *client) ModifyACL(ctx context.Context, p *params.ModifyACLRequest) error { return c.Client.Call(ctx, p, nil) } // SetACL sets the members of the ACL with the requested name. // Only administrators and members of the meta-ACL for the name // may access this endpoint. The meta-ACL for meta-ACLs is "admin". func (c *client) SetACL(ctx context.Context, p *params.SetACLRequest) error { return c.Client.Call(ctx, p, nil) } aclstore-2.1.0/aclclient/client_test.go000066400000000000000000000127311405564600300201370ustar00rootroot00000000000000package aclclient_test import ( "context" "net/http" "net/http/httptest" "sort" "testing" qt "github.com/frankban/quicktest" "github.com/juju/simplekv/memsimplekv" errgo "gopkg.in/errgo.v1" httprequest "gopkg.in/httprequest.v1" aclstore "github.com/juju/aclstore/v2" "github.com/juju/aclstore/v2/aclclient" "github.com/juju/aclstore/v2/params" ) func TestGet(t *testing.T) { ctx := context.Background() c := qt.New(t) manager, srv, client := newServer(ctx, c) defer srv.Close() err := manager.CreateACL(ctx, "test", "test1", "test2", "test3") c.Assert(err, qt.Equals, nil) users, err := client.Get(ctx, "test") c.Assert(err, qt.Equals, nil) c.Assert(users, qt.DeepEquals, []string{"test1", "test2", "test3"}) } func TestGetError(t *testing.T) { ctx := context.Background() c := qt.New(t) _, srv, client := newServer(ctx, c) defer srv.Close() users, err := client.Get(ctx, "test") c.Assert(err, qt.ErrorMatches, `Get http.*/test: ACL not found`) rerr, ok := errgo.Cause(err).(*httprequest.RemoteError) c.Assert(ok, qt.Equals, true, qt.Commentf("unexpected error cause %T", errgo.Cause(err))) c.Assert(rerr.Code, qt.Equals, aclstore.CodeACLNotFound) c.Assert(users, qt.IsNil) } func TestSet(t *testing.T) { ctx := context.Background() c := qt.New(t) manager, srv, client := newServer(ctx, c) defer srv.Close() err := manager.CreateACL(ctx, "test", "test1", "test2", "test3") c.Assert(err, qt.Equals, nil) err = client.Set(ctx, "test", []string{"test4", "test5", "test6"}) c.Assert(err, qt.Equals, nil) users, err := client.Get(ctx, "test") c.Assert(err, qt.Equals, nil) c.Assert(users, qt.DeepEquals, []string{"test4", "test5", "test6"}) } func TestSetError(t *testing.T) { ctx := context.Background() c := qt.New(t) _, srv, client := newServer(ctx, c) defer srv.Close() err := client.Set(ctx, "test", []string{"test4", "test5", "test6"}) c.Assert(err, qt.ErrorMatches, `Put http.*/test: ACL not found`) rerr, ok := errgo.Cause(err).(*httprequest.RemoteError) c.Assert(ok, qt.Equals, true, qt.Commentf("unexpected error cause %T", errgo.Cause(err))) c.Assert(rerr.Code, qt.Equals, aclstore.CodeACLNotFound) } func TestAdd(t *testing.T) { ctx := context.Background() c := qt.New(t) manager, srv, client := newServer(ctx, c) defer srv.Close() err := manager.CreateACL(ctx, "test", "test1", "test2", "test3") c.Assert(err, qt.Equals, nil) err = client.Add(ctx, "test", []string{"test4", "test5", "test6"}) c.Assert(err, qt.Equals, nil) users, err := client.Get(ctx, "test") c.Assert(err, qt.Equals, nil) c.Assert(users, qt.DeepEquals, []string{"test1", "test2", "test3", "test4", "test5", "test6"}) } func TestAddError(t *testing.T) { ctx := context.Background() c := qt.New(t) _, srv, client := newServer(ctx, c) defer srv.Close() err := client.Add(ctx, "test", []string{"test4", "test5", "test6"}) c.Assert(err, qt.ErrorMatches, `Post http.*/test: ACL not found`) rerr, ok := errgo.Cause(err).(*httprequest.RemoteError) c.Assert(ok, qt.Equals, true, qt.Commentf("unexpected error cause %T", errgo.Cause(err))) c.Assert(rerr.Code, qt.Equals, aclstore.CodeACLNotFound) } func TestRemove(t *testing.T) { ctx := context.Background() c := qt.New(t) manager, srv, client := newServer(ctx, c) defer srv.Close() err := manager.CreateACL(ctx, "test", "test1", "test2", "test3", "test4", "test5", "test6") c.Assert(err, qt.Equals, nil) err = client.Remove(ctx, "test", []string{"test4", "test5", "test6"}) c.Assert(err, qt.Equals, nil) users, err := client.Get(ctx, "test") c.Assert(err, qt.Equals, nil) c.Assert(users, qt.DeepEquals, []string{"test1", "test2", "test3"}) } func TestRemoveError(t *testing.T) { ctx := context.Background() c := qt.New(t) _, srv, client := newServer(ctx, c) defer srv.Close() err := client.Remove(ctx, "test", []string{"test4", "test5", "test6"}) c.Assert(err, qt.ErrorMatches, `Post http.*/test: ACL not found`) rerr, ok := errgo.Cause(err).(*httprequest.RemoteError) c.Assert(ok, qt.Equals, true, qt.Commentf("unexpected error cause %T", errgo.Cause(err))) c.Assert(rerr.Code, qt.Equals, aclstore.CodeACLNotFound) } func TestGetACLs(t *testing.T) { ctx := context.Background() c := qt.New(t) manager, srv, client := newServer(ctx, c) defer srv.Close() err := manager.CreateACL(ctx, "test1", "test1", "test2", "test3") c.Assert(err, qt.Equals, nil) err = manager.CreateACL(ctx, "test2", "test1", "test2", "test3") c.Assert(err, qt.Equals, nil) err = manager.CreateACL(ctx, "test3", "test1", "test2", "test3") c.Assert(err, qt.Equals, nil) acls, err := client.GetACLs(ctx, ¶ms.GetACLsRequest{}) c.Assert(err, qt.Equals, nil) sort.Strings(acls.ACLs) c.Assert(acls.ACLs, qt.DeepEquals, []string{"_test1", "_test2", "_test3", "admin", "test1", "test2", "test3"}) } func newServer(ctx context.Context, c *qt.C) (*aclstore.Manager, *httptest.Server, *aclclient.Client) { store := aclstore.NewACLStore(memsimplekv.NewStore()) manager, err := aclstore.NewManager(ctx, aclstore.Params{ Store: store, InitialAdminUsers: []string{"test-admin"}, }) c.Assert(err, qt.Equals, nil) srv := httptest.NewServer(manager.NewHandler(aclstore.HandlerParams{ Authenticate: func(ctx context.Context, w http.ResponseWriter, req *http.Request) (aclstore.Identity, error) { return allowed{}, nil }, })) client := aclclient.New(aclclient.NewParams{ BaseURL: srv.URL, Doer: srv.Client(), }) return manager, srv, client } type allowed struct{} func (allowed) Allow(context.Context, []string) (bool, error) { return true, nil } aclstore-2.1.0/dependencies.tsv000066400000000000000000000014511405564600300165160ustar00rootroot00000000000000github.com/frankban/quicktest git 46f57ea7cf55c745b1c629f3f04353ca256cae1e 2021-05-07T18:13:00Z github.com/google/go-cmp git 8099a9787ce5dc5984ed879a3bda47dc730a8e97 2017-08-03T17:35:09Z github.com/juju/simplekv git 78efc06eb396a438ff13c00f0d2015dacc136b23 2021-06-01T09:31:06Z github.com/julienschmidt/httprouter git 77a895ad01ebc98a4dc95d8355bc825ce80a56f6 2015-10-13T22:55:20Z github.com/kr/pretty git cfb55aafdaf3ec08f0db22699ab822c50091b1c4 2016-08-23T17:07:15Z github.com/kr/text git 7cafcd837844e784b526369c9bce262804aebc60 2016-05-04T23:40:17Z golang.org/x/net git d25186b37f34ebdbbea8f488ef055638dfab272d 2018-03-06T06:01:52Z gopkg.in/errgo.v1 git 442357a80af5c6bf9b6d51ae791a39c3421004f3 2016-12-22T12:58:16Z gopkg.in/httprequest.v1 git a1015531595ff2ed1a0c126b55da352e6923eaac 2018-06-25T11:31:07Z aclstore-2.1.0/go.mod000066400000000000000000000006341405564600300144420ustar00rootroot00000000000000module github.com/juju/aclstore/v2 go 1.16 require ( github.com/frankban/quicktest v1.13.0 github.com/google/go-cmp v0.5.6 // indirect github.com/juju/simplekv v1.1.0 github.com/julienschmidt/httprouter v0.0.0-20151013225520-77a895ad01eb github.com/kr/text v0.2.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/errgo.v1 v1.0.1 gopkg.in/httprequest.v1 v1.1.2 ) aclstore-2.1.0/go.sum000066400000000000000000000167131405564600300144740ustar00rootroot00000000000000github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/frankban/quicktest v1.1.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.1.1/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/juju/clock v0.0.0-20180808021310-bab88fc67299/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c h1:3UvYABOQRhJAApj9MdCN+Ydv841ETSoy6xLzdmmr/9A= github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= github.com/juju/errors v0.0.0-20180726005433-812b06ada177/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20190207033735-e65537c515d7 h1:dMIPRDg6gi7CUp0Kj2+HxqJ5kTr1iAdzsXYIrLCNSmU= github.com/juju/errors v0.0.0-20190207033735-e65537c515d7/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20190212223446-d976af380377 h1:n6QjW3g5JNY3xPmIjFt6z1H6tFQA6BhwOC2bvTAm1YU= github.com/juju/loggo v0.0.0-20190212223446-d976af380377/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208/go.mod h1:0OChplkvPTZ174D2FYZXg4IB9hbEwyHkD+zT+/eK+Fg= github.com/juju/mgotest v1.0.2/go.mod h1:04v1Xi2RiTO3h77YWtaXB2LAaGRSSi+Vl4hOV1coD0k= github.com/juju/postgrestest v1.1.0/go.mod h1:/n17Y2T6iFozzXwSCO0JYJ5gSiz2caEtSwAjh/uLXDM= github.com/juju/retry v0.0.0-20160928201858-1998d01ba1c3/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= github.com/juju/retry v0.0.0-20180821225755-9058e192b216 h1:/eQL7EJQKFHByJe3DeE8Z36yqManj9UY5zppDoQi4FU= github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= github.com/juju/simplekv v1.0.1 h1:yCPB9Lr1IewsRED/YoP/0lSAZ3KwW7hkxnGk+5ZK2Pg= github.com/juju/simplekv v1.0.1/go.mod h1:DU+ikvsVozbxyiT0K1/jGGwvcTlXvgwwXiL1obBbnIA= github.com/juju/simplekv v1.1.0 h1:3j2a817FVp1uwwc7Y0+f9Bok2HSBoyLPJKOAzlQ/z0o= github.com/juju/simplekv v1.1.0/go.mod h1:OZjCrSxeKfEpNNp3JtM4B8NOVR4EJTffgvRY1qpPZ+w= github.com/juju/testing v0.0.0-20180517134105-72703b1e95eb/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073 h1:WQM1NildKThwdP7qWrNAFGzp4ijNLw8RlgENkaI4MJs= github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/utils v0.0.0-20180619112806-c746c6e86f4f/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d h1:irPlN9z5VCe6BTsqVsxheCZH99OFSmqSVyTigW4mEoY= github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/version v0.0.0-20180108022336-b64dbd566305 h1:lQxPJ1URr2fjsKnJRt/BxiIxjLt9IKGvS+0injMHbag= github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/julienschmidt/httprouter v0.0.0-20151013225520-77a895ad01eb h1:a8sYyruWLyKeMay8GPF+nwZ36xT7A0oNyn68Q6wJ5cc= github.com/julienschmidt/httprouter v0.0.0-20151013225520-77a895ad01eb/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= gopkg.in/httprequest.v1 v1.1.2 h1:dxQgu0dVQ81s7iAUTYkT7xzxM3y9RkwVgN//UVjYruc= gopkg.in/httprequest.v1 v1.1.2/go.mod h1:/CkavNL+g3qLOrpFHVrEx4NKepeqR4XTZWNj4sGGjz0= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/retry.v1 v1.0.2/go.mod h1:tLRIBNXxoKtalyAWBSIbHdWkIBN2x9jVEm5l0Z+BjXs= gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= aclstore-2.1.0/manager.go000066400000000000000000000221661405564600300153010ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPL, see LICENCE file for details. package aclstore import ( "context" "net/http" "path" "sort" "strings" "github.com/julienschmidt/httprouter" "gopkg.in/errgo.v1" httprequest "gopkg.in/httprequest.v1" "github.com/juju/aclstore/v2/params" ) // Params holds the parameters for a NewManager call. type Params struct { // Store holds the persistent storage used by the handler. Store ACLStore // InitialAdminUsers holds the contents of the admin ACL // when it is first created. InitialAdminUsers []string } // Identity represents an authenticated user. type Identity interface { // Allow reports whether the user should be allowed to access // any of the users or groups in the given ACL slice. Allow(ctx context.Context, acl []string) (bool, error) } // AdminACL holds the name of the administrator ACL. const AdminACL = "admin" // CodeACLNotFound holds the error code returned from // the HTTP endpoints when an ACL name has not been // created. const CodeACLNotFound = "ACL not found" // Manager implements an ACL manager. type Manager struct { p Params } var errAuthenticationFailed = errgo.Newf("authentication failed") var reqServer = &httprequest.Server{ ErrorWriter: func(ctx context.Context, w http.ResponseWriter, err error) { if errgo.Cause(err) == errAuthenticationFailed { // The Authenticate method has already written its response. return } status, body := errorMapper(ctx, err) httprequest.WriteJSON(w, status, body) }, } func errorMapper(ctx context.Context, err error) (int, interface{}) { switch errgo.Cause(err) { case ErrACLNotFound: return http.StatusNotFound, &httprequest.RemoteError{ Message: err.Error(), Code: CodeACLNotFound, } case ErrBadUsername: err = httprequest.Errorf(httprequest.CodeBadRequest, "%v", err) } return httprequest.DefaultErrorMapper(ctx, err) } // NewManager returns a new Manager instance that manages a // set of ACLs. It ensures there is at least one ACL // created, named "admin", which is given p.InitialAdminUsers // when it is first created. func NewManager(ctx context.Context, p Params) (*Manager, error) { if err := p.Store.CreateACL(ctx, AdminACL, p.InitialAdminUsers); err != nil { return nil, errgo.Notef(err, "cannot create initial admin ACL") } m := &Manager{ p: p, } return m, nil } // ACL returns the members of the given ACL. func (m *Manager) ACL(ctx context.Context, name string) ([]string, error) { // TODO implement a cache to avoid hitting the underlying // store each time. return m.p.Store.Get(ctx, name) } // CreateACL creates an ACL with the given name. It also creates an ACL // _name which is the ACL that guards membership of the ACL itself. Any // member of _name or any member of the admin ACL may change the // membership of ACL name. Only members of the admin ACL may change the // membership of _name. // // The name itself must not start with an underscore. // // This does nothing if an ACL with that name already exists. func (h *Manager) CreateACL(ctx context.Context, name string, initialUsers ...string) error { if isMetaName(name) { return errgo.Newf("invalid ACL name %q", name) } if err := h.p.Store.CreateACL(ctx, name, initialUsers); err != nil { return errgo.Mask(err) } if err := h.p.Store.CreateACL(ctx, metaName(name), nil); err != nil { return errgo.Mask(err) } return nil } // aclName is implemented by the request parameters for all endpoints // to return the associated ACL name. type aclName interface { ACLName() string } // HandlerParams holds the parameters for a NewHandler call. type HandlerParams struct { // RootPath holds the root URL path prefix to use // for the ACL endpoints. All the endpoints will be // prefixed with this path. RootPath string // Authenticate authenticates the given HTTP request and returns // the resulting authenticated identity. If authentication // fails, Authenticate should write its own response and return // an error. Authenticate func(ctx context.Context, w http.ResponseWriter, req *http.Request) (Identity, error) } // NewHandler creates an ACL administration interface that allows clients // to manipulate the ACLs. The set of ACLs that can be manipulated can be // changed with the Manager.CreateACL method. func (m *Manager) NewHandler(p HandlerParams) http.Handler { h := &handler{ p: p, m: m, router: httprouter.New(), } h.router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { httprequest.WriteJSON(w, http.StatusNotFound, &httprequest.RemoteError{ Message: "URL path not found", Code: httprequest.CodeNotFound, }) }) for _, ep := range reqServer.Handlers(h.newHandler) { h.router.Handle(ep.Method, path.Join(p.RootPath, ep.Path), ep.Handle) } return h } type handler struct { p HandlerParams m *Manager router *httprouter.Router } // ServeHTTP implements http.Handler. func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { h.router.ServeHTTP(w, req) } type handler1 struct { h *handler } // newHandler returns a handler instance to serve a particular HTTP request. func (h *handler) newHandler(p httprequest.Params, arg aclName) (handler1, context.Context, error) { ctx := p.Context if err := h.authorizeRequest(ctx, p, arg.ACLName()); err != nil { return handler1{}, nil, errgo.Mask(err, errgo.Any) } return handler1{ h: h, }, p.Context, nil } // authorizeRequest checks that an HTTP request is authorized. If the // authorization failed because Authenticate failed, it returns an error // with an errAuthenticationFailed cause to signal that the desired // error response has already been written. func (h *handler) authorizeRequest(ctx context.Context, p httprequest.Params, aclName string) error { if aclName == "" { return httprequest.Errorf(httprequest.CodeBadRequest, "empty ACL name") } identity, err := h.p.Authenticate(ctx, p.Response, p.Request) if err != nil { return errAuthenticationFailed } var checkACLName string if aclName == AdminACL || isMetaName(aclName) { // We're trying to access either the admin ACL or a meta-ACL; for either // of these, admin privileges are needed. checkACLName = AdminACL } else { // For all normal ACLs, access for a given ACL name is decided via membership // of the meta-ACL for that name. checkACLName = metaName(aclName) } acl, err := h.m.ACL(ctx, checkACLName) if err != nil { return errgo.Mask(err, errgo.Is(ErrACLNotFound)) } if checkACLName != AdminACL { // Admin users always get permission to do anything. adminACL, err := h.m.ACL(ctx, AdminACL) if err != nil { return errgo.Notef(err, "cannot get admin ACL") } acl = append(acl, adminACL...) } ok, err := identity.Allow(ctx, acl) if err != nil { return errgo.Notef(err, "cannot check permissions") } if !ok { return httprequest.Errorf(httprequest.CodeForbidden, "") } return nil } // GetACL returns the members of the ACL with the requested name. // Only administrators and members of the meta-ACL for the name // may access this endpoint. The meta-ACL for meta-ACLs is "admin". func (h handler1) GetACL(p httprequest.Params, req *params.GetACLRequest) (*params.GetACLResponse, error) { users, err := h.h.m.p.Store.Get(p.Context, req.Name) if err != nil { return nil, errgo.Mask(err, errgo.Is(ErrACLNotFound)) } return ¶ms.GetACLResponse{ Users: users, }, nil } // SetACL sets the members of the ACL with the requested name. // Only administrators and members of the meta-ACL for the name // may access this endpoint. The meta-ACL for meta-ACLs is "admin". func (h handler1) SetACL(p httprequest.Params, req *params.SetACLRequest) error { err := h.h.m.p.Store.Set(p.Context, req.Name, req.Body.Users) return errgo.Mask(err, errgo.Is(ErrACLNotFound), errgo.Is(ErrBadUsername)) } // ModifyACL modifies the members of the ACL with the requested name. // Only administrators and members of the meta-ACL for the name // may access this endpoint. The meta-ACL for meta-ACLs is "admin". func (h handler1) ModifyACL(p httprequest.Params, req *params.ModifyACLRequest) error { switch { case len(req.Body.Add) > 0 && len(req.Body.Remove) > 0: return httprequest.Errorf(httprequest.CodeBadRequest, "cannot add and remove users at the same time") case len(req.Body.Add) > 0: err := h.h.m.p.Store.Add(p.Context, req.Name, req.Body.Add) return errgo.Mask(err, errgo.Is(ErrACLNotFound), errgo.Is(ErrBadUsername)) case len(req.Body.Remove) > 0: err := h.h.m.p.Store.Remove(p.Context, req.Name, req.Body.Remove) return errgo.Mask(err, errgo.Is(ErrACLNotFound), errgo.Is(ErrBadUsername)) default: return nil } } // GetACLs returns the list of all ACLs. // Only administrators may access this endpoint. func (h handler1) GetACLs(p httprequest.Params, req *params.GetACLsRequest) (*params.GetACLsResponse, error) { lister, ok := h.h.m.p.Store.(ACLLister) if !ok { return nil, errgo.Newf("cannot list ACLs") } acls, err := lister.ACLs(p.Context) if err != nil { return nil, errgo.Mask(err) } sort.Strings(acls) return ¶ms.GetACLsResponse{ ACLs: acls, }, nil } func metaName(aclName string) string { return "_" + aclName } func isMetaName(aclName string) bool { return strings.HasPrefix(aclName, "_") } aclstore-2.1.0/manager_test.go000066400000000000000000000373371405564600300163460ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPL, see LICENCE file for details. package aclstore_test import ( "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "reflect" "sort" "testing" qt "github.com/frankban/quicktest" "github.com/juju/simplekv/memsimplekv" "gopkg.in/errgo.v1" httprequest "gopkg.in/httprequest.v1" aclstore "github.com/juju/aclstore/v2" "github.com/juju/aclstore/v2/aclclient" "github.com/juju/aclstore/v2/params" ) var getACLTests = []struct { testName string path string rootPath string users map[string][]string expectCheckACL []string expectStatus int expectResponse interface{} }{{ testName: "get_admin_ACL", rootPath: "/root", path: "/root/admin", users: map[string][]string{ "admin": {"alice", "bob"}, }, expectCheckACL: []string{"alice", "bob"}, expectStatus: http.StatusOK, expectResponse: map[string][]string{ "users": {"alice", "bob"}, }, }, { testName: "get_ACL_with_empty_root_path", path: "/admin", users: map[string][]string{ "admin": {"alice", "bob"}, }, expectCheckACL: []string{"alice", "bob"}, expectStatus: http.StatusOK, expectResponse: map[string][]string{ "users": {"alice", "bob"}, }, }, { testName: "get_outside_of_root", rootPath: "/root", path: "/blah/foo", expectStatus: http.StatusNotFound, expectResponse: httprequest.RemoteError{ Message: "URL path not found", Code: httprequest.CodeNotFound, }, }, { testName: "get_outside_of_root_with_root_as_prefix", rootPath: "/root", path: "/rootfoo/admin", expectStatus: http.StatusNotFound, expectResponse: httprequest.RemoteError{ Message: "URL path not found", Code: httprequest.CodeNotFound, }, }, { testName: "get_nonexistent_ACL", rootPath: "/root", path: "/root/nonexistent", expectStatus: http.StatusNotFound, expectResponse: httprequest.RemoteError{ Message: "ACL not found", Code: aclstore.CodeACLNotFound, }, }, { testName: "get_nonadmin_ACL", rootPath: "/root", users: map[string][]string{ "admin": {"alice", "bob"}, "someacl": {"charlie", "daisy"}, "_someacl": {"claire", "ed"}, }, path: "/root/someacl", expectCheckACL: []string{"claire", "ed", "alice", "bob"}, expectStatus: http.StatusOK, expectResponse: map[string][]string{ "users": {"charlie", "daisy"}, }, }, { testName: "get_nonadmin_meta_ACL", rootPath: "/root", users: map[string][]string{ "admin": {"alice", "bob"}, "someacl": {"charlie", "daisy"}, "_someacl": {"claire", "ed"}, }, path: "/root/_someacl", expectCheckACL: []string{"alice", "bob"}, expectStatus: http.StatusOK, expectResponse: map[string][]string{ "users": {"claire", "ed"}, }, }, { testName: "get_all_ACLs", rootPath: "/root", path: "/root/", users: map[string][]string{ "admin": {"alice", "bob"}, "read": {"eve"}, }, expectCheckACL: []string{"alice", "bob"}, expectStatus: http.StatusOK, expectResponse: map[string][]string{ "acls": {"admin", "read"}, }, }} func TestGetACL(t *testing.T) { c := qt.New(t) for _, test := range getACLTests { c.Run(test.testName, func(c *qt.C) { var checkedACL []string _, h := managerWithACLs(c, test.rootPath, test.users, &checkedACL) srv := httptest.NewServer(h) defer srv.Close() assertJSONCall(c, "GET", srv.URL+test.path, nil, test.expectStatus, test.expectResponse) c.Assert(checkedACL, qt.DeepEquals, test.expectCheckACL) }) } } var setACLTests = []struct { testName string path string users map[string][]string setACL []string expectCheckACL []string expectACLName string expectACL []string expectStatus int expectResponse interface{} }{{ testName: "set_admin_ACL", users: map[string][]string{ "admin": {"alice", "bob"}, }, path: "/root/admin", setACL: []string{"foo", "bar", "alice"}, expectCheckACL: []string{"alice", "bob"}, expectACLName: "admin", expectACL: []string{"alice", "bar", "foo"}, expectStatus: http.StatusOK, }, { testName: "set_nonexistent_ACL", path: "/root/nonexistent", expectStatus: http.StatusNotFound, expectResponse: httprequest.RemoteError{ Message: "ACL not found", Code: aclstore.CodeACLNotFound, }, }, { testName: "set_non_admin_ACL", users: map[string][]string{ "admin": {"boss"}, "someacl": {"charlie", "daisy"}, "_someacl": {"a", "b"}, }, path: "/root/someacl", setACL: []string{"elouise", "fred"}, expectCheckACL: []string{"a", "b", "boss"}, expectACLName: "someacl", expectACL: []string{"elouise", "fred"}, expectStatus: http.StatusOK, }, { testName: "set_meta_ACL", users: map[string][]string{ "admin": {"boss"}, "someacl": {"charlie", "daisy"}, "_someacl": {"a", "b"}, }, path: "/root/_someacl", setACL: []string{"daisy"}, expectCheckACL: []string{"boss"}, expectACLName: "_someacl", expectACL: []string{"daisy"}, expectStatus: http.StatusOK, }, { testName: "set_ACL_with_invalid_user", users: map[string][]string{ "admin": {"boss"}, }, path: "/root/admin", setACL: []string{"daisy", ""}, expectCheckACL: []string{"boss"}, expectStatus: http.StatusBadRequest, expectResponse: httprequest.RemoteError{ Message: `invalid user name ""`, Code: httprequest.CodeBadRequest, }, }} func TestSetACL(t *testing.T) { c := qt.New(t) for _, test := range setACLTests { c.Run(test.testName, func(c *qt.C) { var checkedACL []string m, h := managerWithACLs(c, "/root", test.users, &checkedACL) srv := httptest.NewServer(h) defer srv.Close() assertJSONCall(c, "PUT", srv.URL+test.path, map[string][]string{ "users": test.setACL, }, test.expectStatus, test.expectResponse) c.Assert(checkedACL, qt.DeepEquals, test.expectCheckACL) if test.expectACLName != "" { gotACL, err := m.ACL(context.Background(), test.expectACLName) c.Assert(err, qt.Equals, nil) c.Assert(gotACL, qt.DeepEquals, test.expectACL) } }) } } var modifyACLTests = []struct { testName string path string users map[string][]string addUsers []string removeUsers []string expectCheckACL []string expectACLName string expectACL []string expectStatus int expectResponse interface{} }{{ testName: "add_admin_ACL", users: map[string][]string{ "admin": {"alice", "bob"}, }, path: "/root/admin", addUsers: []string{"foo", "bar", "alice"}, expectCheckACL: []string{"alice", "bob"}, expectACLName: "admin", expectACL: []string{"alice", "bar", "bob", "foo"}, expectStatus: http.StatusOK, }, { testName: "remove_admin_ACL", users: map[string][]string{ "admin": {"alice", "bob"}, }, path: "/root/admin", removeUsers: []string{"bar", "alice"}, expectCheckACL: []string{"alice", "bob"}, expectACLName: "admin", expectACL: []string{"bob"}, expectStatus: http.StatusOK, }, { testName: "set_nonexistent_ACL", path: "/root/nonexistent", expectStatus: http.StatusNotFound, expectResponse: httprequest.RemoteError{ Message: "ACL not found", Code: aclstore.CodeACLNotFound, }, }, { testName: "remove_and_add", users: map[string][]string{ "admin": {"alice", "bob"}, }, path: "/root/admin", addUsers: []string{"edward"}, removeUsers: []string{"bar"}, expectCheckACL: []string{"alice", "bob"}, expectStatus: http.StatusBadRequest, expectResponse: &httprequest.RemoteError{ Message: `cannot add and remove users at the same time`, Code: httprequest.CodeBadRequest, }, }, { testName: "add_to_non_admin_ACL", users: map[string][]string{ "admin": {"boss"}, "someacl": {"charlie", "daisy"}, "_someacl": {"a", "b"}, }, path: "/root/someacl", addUsers: []string{"elouise", "fred"}, expectCheckACL: []string{"a", "b", "boss"}, expectACLName: "someacl", expectACL: []string{"charlie", "daisy", "elouise", "fred"}, expectStatus: http.StatusOK, }, { testName: "add_to_meta_ACL", users: map[string][]string{ "admin": {"boss"}, "someacl": {"charlie", "daisy"}, "_someacl": {"a", "b"}, }, path: "/root/_someacl", addUsers: []string{"charlie"}, expectCheckACL: []string{"boss"}, expectACLName: "_someacl", expectACL: []string{"a", "b", "charlie"}, expectStatus: http.StatusOK, }, { testName: "add_invalid_user", users: map[string][]string{ "admin": {"boss"}, }, path: "/root/admin", addUsers: []string{"daisy", ""}, expectCheckACL: []string{"boss"}, expectStatus: http.StatusBadRequest, expectResponse: httprequest.RemoteError{ Message: `invalid user name ""`, Code: httprequest.CodeBadRequest, }, }} func TestModifyACL(t *testing.T) { c := qt.New(t) for _, test := range modifyACLTests { c.Run(test.testName, func(c *qt.C) { var checkedACL []string m, h := managerWithACLs(c, "/root", test.users, &checkedACL) srv := httptest.NewServer(h) defer srv.Close() assertJSONCall(c, "POST", srv.URL+test.path, map[string][]string{ "add": test.addUsers, "remove": test.removeUsers, }, test.expectStatus, test.expectResponse) c.Assert(checkedACL, qt.DeepEquals, test.expectCheckACL) if test.expectACLName != "" { gotACL, err := m.ACL(context.Background(), test.expectACLName) c.Assert(err, qt.Equals, nil) c.Assert(gotACL, qt.DeepEquals, test.expectACL) } }) } } func TestWithAuthenticate(t *testing.T) { ctx := context.Background() c := qt.New(t) m, err := aclstore.NewManager(ctx, aclstore.Params{ Store: aclstore.NewACLStore(memsimplekv.NewStore()), InitialAdminUsers: []string{"bob"}, }) c.Assert(err, qt.Equals, nil) h := m.NewHandler(aclstore.HandlerParams{ RootPath: "/rootpath", Authenticate: func(ctx context.Context, w http.ResponseWriter, req *http.Request) (aclstore.Identity, error) { req.ParseForm() user := req.Form.Get("auth") if user == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusTeapot) fmt.Fprintf(w, `"go away: %s"`, req.URL.Path) return nil, errgo.Newf("no auth header found") } return identityFunc(func(ctx context.Context, acl []string) (bool, error) { for _, a := range acl { if a == user { return true, nil } } return false, nil }), nil }, }) srv := httptest.NewServer(h) defer srv.Close() assertJSONCall(c, "GET", srv.URL+"/rootpath/admin", nil, http.StatusTeapot, "go away: /rootpath/admin") assertJSONCall(c, "GET", srv.URL+"/rootpath/admin?auth=bob", nil, http.StatusOK, params.GetACLResponse{ Users: []string{"bob"}, }) } func TestForbidden(t *testing.T) { ctx := context.Background() c := qt.New(t) m, err := aclstore.NewManager(ctx, aclstore.Params{ Store: aclstore.NewACLStore(memsimplekv.NewStore()), InitialAdminUsers: []string{"bob"}, }) c.Assert(err, qt.Equals, nil) h := m.NewHandler(aclstore.HandlerParams{ Authenticate: func(ctx context.Context, w http.ResponseWriter, req *http.Request) (aclstore.Identity, error) { return identityFunc(func(ctx context.Context, acl []string) (bool, error) { return false, nil }), nil }, }) srv := httptest.NewServer(h) defer srv.Close() assertJSONCall(c, "GET", srv.URL+"/admin", nil, http.StatusForbidden, &httprequest.RemoteError{ Code: httprequest.CodeForbidden, Message: httprequest.CodeForbidden, }) } func TestManagerCreateACL(t *testing.T) { c := qt.New(t) var checkedACL []string ctx := context.Background() m, _ := managerWithACLs(c, "", nil, &checkedACL) err := m.CreateACL(ctx, "foo", "x", "y") c.Assert(err, qt.Equals, nil) acl, err := m.ACL(ctx, "foo") c.Assert(err, qt.Equals, nil) c.Assert(acl, qt.DeepEquals, []string{"x", "y"}) err = m.CreateACL(ctx, "foo", "z", "w") c.Assert(err, qt.Equals, nil) acl, err = m.ACL(ctx, "foo") c.Assert(err, qt.Equals, nil) c.Assert(acl, qt.DeepEquals, []string{"x", "y"}) // Check that the meta ACL is created too. acl, err = m.ACL(ctx, "_foo") c.Assert(err, qt.Equals, nil) c.Assert(acl, qt.DeepEquals, []string(nil)) } func TestManagerCreateACLWithInvalidACLName(t *testing.T) { c := qt.New(t) var checkedACL []string ctx := context.Background() m, _ := managerWithACLs(c, "", nil, &checkedACL) err := m.CreateACL(ctx, "_foo", "x", "y") c.Assert(err, qt.ErrorMatches, `invalid ACL name "_foo"`) } func TestGetACLs(t *testing.T) { ctx := context.Background() c := qt.New(t) m, err := aclstore.NewManager(ctx, aclstore.Params{ Store: aclstore.NewACLStore(memsimplekv.NewStore()), InitialAdminUsers: []string{"test-admin"}, }) c.Assert(err, qt.Equals, nil) h := m.NewHandler(aclstore.HandlerParams{ RootPath: "/rootpath", Authenticate: func(ctx context.Context, w http.ResponseWriter, req *http.Request) (aclstore.Identity, error) { return allowed{}, nil }, }) srv := httptest.NewServer(h) defer srv.Close() client := aclclient.New(aclclient.NewParams{ BaseURL: srv.URL + "/rootpath", Doer: srv.Client(), }) err = m.CreateACL(ctx, "test1", "user1") c.Assert(err, qt.Equals, nil) err = m.CreateACL(ctx, "test2", "user1") c.Assert(err, qt.Equals, nil) err = m.CreateACL(ctx, "test3", "user1") c.Assert(err, qt.Equals, nil) acls, err := client.GetACLs(ctx, ¶ms.GetACLsRequest{}) c.Assert(err, qt.Equals, nil) sort.Strings(acls.ACLs) c.Assert(acls.ACLs, qt.DeepEquals, []string{"_test1", "_test2", "_test3", "admin", "test1", "test2", "test3"}) } type allowed struct{} func (allowed) Allow(context.Context, []string) (bool, error) { return true, nil } // managerWithACLs returns a Manager instance running an ACL manager // primed with the given ACLs. When an ACL is checked, *checkedACL is set // to the ACL that's checked. func managerWithACLs(c *qt.C, rootPath string, acls map[string][]string, checkedACL *[]string) (*aclstore.Manager, http.Handler) { ctx := context.Background() store := aclstore.NewACLStore(memsimplekv.NewStore()) for aclName, users := range acls { err := store.CreateACL(ctx, aclName, users) c.Assert(err, qt.Equals, nil) } m, err := aclstore.NewManager(ctx, aclstore.Params{ Store: store, }) h := m.NewHandler(aclstore.HandlerParams{ RootPath: rootPath, Authenticate: func(ctx context.Context, w http.ResponseWriter, req *http.Request) (aclstore.Identity, error) { return identityFunc(func(ctx context.Context, acl []string) (bool, error) { *checkedACL = acl return true, nil }), nil }, }) c.Assert(err, qt.Equals, nil) return m, h } type identityFunc func(ctx context.Context, acl []string) (bool, error) func (f identityFunc) Allow(ctx context.Context, acl []string) (bool, error) { return f(ctx, acl) } // assertJSONCall asserts that when the given handler is called with // the given parameters, the result is as specified. func assertJSONCall(c *qt.C, method, url string, body interface{}, expectStatus int, expectResponse interface{}) { var bodyr io.Reader if body != nil { bodyData, err := json.Marshal(body) c.Assert(err, qt.Equals, nil) bodyr = bytes.NewReader(bodyData) } req, err := http.NewRequest(method, url, bodyr) c.Assert(err, qt.Equals, nil) if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := http.DefaultClient.Do(req) c.Assert(err, qt.Equals, nil) defer resp.Body.Close() respData, err := ioutil.ReadAll(resp.Body) c.Assert(err, qt.Equals, nil) if expectResponse == nil { c.Assert(respData, qt.HasLen, 0, qt.Commentf("body: %s", respData)) return } c.Assert(resp.StatusCode, qt.Equals, expectStatus, qt.Commentf("body: %s", respData)) c.Assert(resp.Header.Get("Content-Type"), qt.Equals, "application/json") respValue := reflect.New(reflect.TypeOf(expectResponse)) err = json.Unmarshal(respData, respValue.Interface()) c.Assert(err, qt.Equals, nil) c.Assert(respValue.Elem().Interface(), qt.DeepEquals, expectResponse) } aclstore-2.1.0/params/000077500000000000000000000000001405564600300146145ustar00rootroot00000000000000aclstore-2.1.0/params/params.go000066400000000000000000000045271405564600300164360ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPL, see LICENCE file for details. package params import "gopkg.in/httprequest.v1" // SetACLRequest holds parameters for an aclstore.Manager.SetACL call. type SetACLRequest struct { httprequest.Route `httprequest:"PUT /:name"` Body SetACLRequestBody `httprequest:",body"` // Name holds the name of the ACL to change. Name string `httprequest:"name,path"` } // ACLName returns the name of the ACL that's being set. func (r SetACLRequest) ACLName() string { return r.Name } // SetACLRequestBody holds the HTTP body for an aclstore.Manager.SetACL call. type SetACLRequestBody struct { Users []string `json:"users"` } // ModifyACLRequest holds parameters for an aclstore.Manager.ModifyACL call. type ModifyACLRequest struct { httprequest.Route `httprequest:"POST /:name"` Body ModifyACLRequestBody `httprequest:",body"` // Name holds the name of the ACL to change. Name string `httprequest:"name,path"` } // ACLName returns the name of the ACL that's being modified. func (r ModifyACLRequest) ACLName() string { return r.Name } // ModifyACLRequestBody holds the HTTP body for an aclstore.Manager.ModifyACL call. // It is an error for both Add and Remove to be specified at the same time. type ModifyACLRequestBody struct { // Add specifies users to add to the ACL. Add []string `json:"add,omitempty"` // Remove specifies users to remove from the ACL. Remove []string `json:"remove,omitempty"` } // GetACLRequest holds parameters for an aclstore.Manager.GetACL call. type GetACLRequest struct { httprequest.Route `httprequest:"GET /:name"` Name string `httprequest:"name,path"` } // ACLName returns the name of the ACL that's being retrieved. func (r GetACLRequest) ACLName() string { return r.Name } // GetACLResponse holds the response body returned by an aclstore.Manager.GetACL call. type GetACLResponse struct { Users []string `json:"users"` } // GetACLsRequest holds parameters for an aclstore.Manager.GetACLs call. type GetACLsRequest struct { httprequest.Route `httprequest:"GET /"` } // ACLName returns the name of the ACL that's being retrieved. func (r GetACLsRequest) ACLName() string { return "admin" } // GetACLsResponse holds the response body returned by an aclstore.Manager.GetACLs call. type GetACLsResponse struct { ACLs []string `json:"acls"` } aclstore-2.1.0/store.go000066400000000000000000000150461405564600300150220ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPL, see LICENCE file for details. package aclstore import ( "context" "sort" "strings" "time" "github.com/juju/simplekv" "gopkg.in/errgo.v1" ) var ( ErrACLNotFound = errgo.Newf("ACL not found") ErrBadUsername = errgo.Newf("bad username") ) // separator is used as the character to divide usernames in the ACL. // This needs to be a character that's illegal in usernames. const separator = "\n" // ACLStore is the persistent storage interface used by an ACLHandler. type ACLStore interface { // CreateACL creates an ACL with the given name and initial users. // If the ACL already exists, this is a no-op and the initialUsers // argument is ignored. // It may return an error with an ErrBadUsername if the initial users // are not valid. CreateACL(ctx context.Context, aclName string, initialUsers []string) error // Add adds users to the ACL with the given name. // Adding a user that's already in the ACL is a no-op. // It returns an error with an ErrACLNotFound cause if the ACL // does not exist, or with an ErrBadUsername cause if any // of the usernames are not valid. Add(ctx context.Context, aclName string, users []string) error // Remove removes users from the ACL with the given name. // It returns an error with an ErrACLNotFound cause if the ACL // does not exist. It returns an error with an ErrUserNotFound // cause if any of the users do not exist. // TODO should it do nothing in that case? Remove(ctx context.Context, aclName string, users []string) error // Set sets the users held in the ACL with the given name. // It returns an ErrACLNotFound cause if the ACL does not // exist, or with an ErrBadUsername cause if any // of the usernames are not valid. Set(ctx context.Context, aclName string, users []string) error // Get returns the users held in the ACL with the given name, // sorted lexically. It returns an error with an ErrACLNotFound cause // if the ACL does not exist. Get(ctx context.Context, aclName string) ([]string, error) } // ACLLister enables clients to list stored ACLs. type ACLLister interface { ACLs(ctx context.Context) ([]string, error) } // NewACLStore returns an ACLStore implementation that uses an underlying // key-value store for persistent storage. func NewACLStore(kv simplekv.Store) ACLStore { return &kvStore{kv} } type kvStore struct { kv simplekv.Store } var errAlreadyExists = errgo.Newf("ACL already exists") // ACLs implements the ACLLister interface. func (s *kvStore) ACLs(ctx context.Context) ([]string, error) { lister, ok := s.kv.(simplekv.KeyLister) if !ok { return nil, errgo.Newf("cannot list ACLs") } acls, err := lister.Keys(ctx) if err != nil { return nil, errgo.Mask(err) } return acls, nil } // CreateACL implements ACLStore.CreateACL. func (s *kvStore) CreateACL(ctx context.Context, aclName string, initialUsers []string) error { err := s.kv.Update(ctx, aclName, time.Time{}, func(val []byte) ([]byte, error) { if val != nil { return nil, errAlreadyExists } newVal, err := s.aclToValue(initialUsers) if err != nil { return nil, errgo.Mask(err, errgo.Is(ErrBadUsername)) } return newVal, nil }) if err != nil { if errgo.Cause(err) == errAlreadyExists { return nil } return errgo.Mask(err, errgo.Is(ErrBadUsername)) } return nil } // Add implements ACLStore.Add. func (s *kvStore) Add(ctx context.Context, aclName string, users []string) error { err := s.kv.Update(ctx, aclName, time.Time{}, func(val []byte) ([]byte, error) { if val == nil { return nil, errgo.WithCausef(nil, ErrACLNotFound, "") } acl := s.valueToACL(val) acl = append(acl, users...) newVal, err := s.aclToValue(acl) if err != nil { return nil, errgo.Mask(err, errgo.Is(ErrBadUsername)) } return newVal, nil }) if err != nil { return errgo.Mask(err, errgo.Is(ErrACLNotFound), errgo.Is(ErrBadUsername)) } return nil } // Remove implements ACLStore.Remove. func (s *kvStore) Remove(ctx context.Context, aclName string, users []string) error { err := s.kv.Update(ctx, aclName, time.Time{}, func(val []byte) ([]byte, error) { if val == nil { return nil, errgo.WithCausef(nil, ErrACLNotFound, "") } acl := s.valueToACL(val) newACL := make([]string, 0, len(acl)) for _, a := range acl { remove := false for _, r := range users { if r == a { remove = true break } } if !remove { newACL = append(newACL, a) } } newVal, err := s.aclToValue(newACL) if err != nil { return nil, errgo.Mask(err, errgo.Is(ErrBadUsername)) } return newVal, nil }) if err != nil { return errgo.Mask(err, errgo.Is(ErrACLNotFound), errgo.Is(ErrBadUsername)) } return nil } // Set implements ACLStore.Set. func (s *kvStore) Set(ctx context.Context, aclName string, users []string) error { newVal, err := s.aclToValue(users) if err != nil { return errgo.Mask(err, errgo.Is(ErrBadUsername)) } err = s.kv.Update(ctx, aclName, time.Time{}, func(val []byte) ([]byte, error) { if val == nil { return nil, errgo.WithCausef(nil, ErrACLNotFound, "") } return newVal, nil }) if err != nil { return errgo.Mask(err, errgo.Is(ErrACLNotFound)) } return nil } // Get implements ACLStore.Get. func (s *kvStore) Get(ctx context.Context, aclName string) ([]string, error) { val, err := s.kv.Get(ctx, aclName) if err != nil { if errgo.Cause(err) == simplekv.ErrNotFound { return nil, errgo.WithCausef(nil, ErrACLNotFound, "") } return nil, errgo.Mask(err) } return s.valueToACL(val), nil } func (*kvStore) aclToValue(acl []string) ([]byte, error) { if len(acl) == 0 { return nil, nil } acl = canonicalACL(acl) size := 0 for _, a := range acl { size += len(a) if !validUser(a) { return nil, errgo.WithCausef(nil, ErrBadUsername, "invalid user name %q", a) } } out := make([]byte, 0, size+len(acl)) out = append(out, acl[0]...) for _, a := range acl[1:] { out = append(out, separator...) out = append(out, a...) } return out, nil } func (*kvStore) valueToACL(data []byte) []string { if len(data) == 0 { return nil } return strings.Split(string(data), separator) } func canonicalACL(acl []string) []string { if len(acl) < 2 { return acl } needSort := false prev := acl[0] for _, a := range acl[1:] { if a <= prev { needSort = true break } prev = a } if !needSort { return acl } acl1 := make([]string, len(acl)) copy(acl1, acl) sort.Strings(acl1) acl = acl1 j := 1 for i, a := range acl[1:] { if acl[i] == a { continue } acl[j] = a j++ } return acl[:j] } func validUser(u string) bool { return len(u) > 0 && !strings.Contains(u, separator) } aclstore-2.1.0/store_test.go000066400000000000000000000110751405564600300160570ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPL, see LICENCE file for details. package aclstore_test import ( "context" "sort" "testing" qt "github.com/frankban/quicktest" "github.com/juju/simplekv/memsimplekv" "gopkg.in/errgo.v1" aclstore "github.com/juju/aclstore/v2" ) func TestCreateACL(t *testing.T) { ctx := context.Background() c := qt.New(t) store := aclstore.NewACLStore(memsimplekv.NewStore()) err := store.CreateACL(ctx, "foo", []string{"x", "y"}) c.Assert(err, qt.Equals, nil) acl, err := store.Get(ctx, "foo") c.Assert(err, qt.Equals, nil) c.Assert(acl, qt.DeepEquals, []string{"x", "y"}) } func TestNewACLOnExistingACL(t *testing.T) { ctx := context.Background() c := qt.New(t) store := aclstore.NewACLStore(memsimplekv.NewStore()) err := store.CreateACL(ctx, "foo", []string{"x", "y"}) c.Assert(err, qt.Equals, nil) err = store.CreateACL(ctx, "foo", []string{"z", "w"}) c.Assert(err, qt.Equals, nil) acl, err := store.Get(ctx, "foo") c.Assert(err, qt.Equals, nil) c.Assert(acl, qt.DeepEquals, []string{"x", "y"}) } func TestAddToNonExistentACL(t *testing.T) { ctx := context.Background() c := qt.New(t) store := aclstore.NewACLStore(memsimplekv.NewStore()) err := store.Add(ctx, "foo", []string{"x", "y"}) c.Assert(err, qt.ErrorMatches, `ACL not found`) c.Assert(errgo.Cause(err), qt.Equals, aclstore.ErrACLNotFound) } func TestAdd(t *testing.T) { ctx := context.Background() c := qt.New(t) store := aclstore.NewACLStore(memsimplekv.NewStore()) err := store.CreateACL(ctx, "foo", []string{"e", "c"}) c.Assert(err, qt.Equals, nil) err = store.Add(ctx, "foo", []string{"a", "d", "f", "e", "a"}) c.Assert(err, qt.Equals, nil) acl, err := store.Get(ctx, "foo") c.Assert(err, qt.Equals, nil) c.Assert(acl, qt.DeepEquals, []string{"a", "c", "d", "e", "f"}) } func TestRemoveNonExistentACL(t *testing.T) { ctx := context.Background() c := qt.New(t) store := aclstore.NewACLStore(memsimplekv.NewStore()) err := store.Remove(ctx, "foo", []string{"x", "y"}) c.Assert(err, qt.ErrorMatches, `ACL not found`) c.Assert(errgo.Cause(err), qt.Equals, aclstore.ErrACLNotFound) } func TestRemove(t *testing.T) { ctx := context.Background() c := qt.New(t) store := aclstore.NewACLStore(memsimplekv.NewStore()) err := store.CreateACL(ctx, "foo", []string{"a", "b", "c", "d"}) c.Assert(err, qt.Equals, nil) err = store.Remove(ctx, "foo", []string{"b", "c", "e"}) c.Assert(err, qt.Equals, nil) acl, err := store.Get(ctx, "foo") c.Assert(err, qt.Equals, nil) c.Assert(acl, qt.DeepEquals, []string{"a", "d"}) } func TestSetNonExistingACL(t *testing.T) { ctx := context.Background() c := qt.New(t) store := aclstore.NewACLStore(memsimplekv.NewStore()) err := store.Set(ctx, "foo", []string{"x", "y"}) c.Assert(err, qt.ErrorMatches, `ACL not found`) c.Assert(errgo.Cause(err), qt.Equals, aclstore.ErrACLNotFound) } func TestSet(t *testing.T) { ctx := context.Background() c := qt.New(t) store := aclstore.NewACLStore(memsimplekv.NewStore()) err := store.CreateACL(ctx, "foo", []string{"a", "b", "c", "d"}) c.Assert(err, qt.Equals, nil) err = store.Set(ctx, "foo", []string{"b", "c", "e", "e"}) c.Assert(err, qt.Equals, nil) acl, err := store.Get(ctx, "foo") c.Assert(err, qt.Equals, nil) c.Assert(acl, qt.DeepEquals, []string{"b", "c", "e"}) } func TestGetNonExistent(t *testing.T) { ctx := context.Background() c := qt.New(t) store := aclstore.NewACLStore(memsimplekv.NewStore()) acl, err := store.Get(ctx, "foo") c.Assert(err, qt.ErrorMatches, `ACL not found`) c.Assert(errgo.Cause(err), qt.Equals, aclstore.ErrACLNotFound) c.Assert(acl, qt.IsNil) } func TestGetEmpty(t *testing.T) { ctx := context.Background() c := qt.New(t) store := aclstore.NewACLStore(memsimplekv.NewStore()) err := store.CreateACL(ctx, "foo", nil) c.Assert(err, qt.Equals, nil) acl, err := store.Get(ctx, "foo") c.Assert(err, qt.Equals, nil) c.Assert(acl, qt.HasLen, 0) } func TestACLLister(t *testing.T) { ctx := context.Background() c := qt.New(t) store := aclstore.NewACLStore(memsimplekv.NewStore()) lister, ok := store.(aclstore.ACLLister) c.Assert(ok, qt.Equals, true) acls, err := lister.ACLs(ctx) c.Assert(err, qt.Equals, nil) c.Assert(acls, qt.HasLen, 0) err = store.CreateACL(ctx, "foo", []string{"x", "y"}) c.Assert(err, qt.Equals, nil) err = store.CreateACL(ctx, "bar", []string{"x", "y"}) c.Assert(err, qt.Equals, nil) err = store.CreateACL(ctx, "choo", []string{"x", "y"}) c.Assert(err, qt.Equals, nil) acls, err = lister.ACLs(ctx) c.Assert(err, qt.Equals, nil) sort.Strings(acls) c.Assert(acls, qt.DeepEquals, []string{"bar", "choo", "foo"}) }