aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2021-11-21 20:54:22 -0800
committerMike Crute <mike@crute.us>2021-11-21 20:54:22 -0800
commit3a5a7e108d9b20f7ef6a7e4bb8439f6e2ba65fa5 (patch)
treef0a0b65473ada73988c8e4aa73261486ac96a158 /app
parent0049bdd2ab6b6b743e9a0cf89f6cbabc8b08e2d4 (diff)
downloadcloud-identity-broker-3a5a7e108d9b20f7ef6a7e4bb8439f6e2ba65fa5.tar.bz2
cloud-identity-broker-3a5a7e108d9b20f7ef6a7e4bb8439f6e2ba65fa5.tar.xz
cloud-identity-broker-3a5a7e108d9b20f7ef6a7e4bb8439f6e2ba65fa5.zip
Add cloud account CRUD endpoints
Diffstat (limited to 'app')
-rw-r--r--app/controllers/api_account.go207
-rw-r--r--app/controllers/api_account_list.go2
-rw-r--r--app/models/account.go95
3 files changed, 273 insertions, 31 deletions
diff --git a/app/controllers/api_account.go b/app/controllers/api_account.go
new file mode 100644
index 0000000..259a7d4
--- /dev/null
+++ b/app/controllers/api_account.go
@@ -0,0 +1,207 @@
1package controllers
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "time"
8
9 "code.crute.us/mcrute/cloud-identity-broker/app/middleware"
10 "code.crute.us/mcrute/cloud-identity-broker/app/models"
11 "code.crute.us/mcrute/cloud-identity-broker/cloud/aws"
12
13 glecho "code.crute.us/mcrute/golib/echo"
14 "code.crute.us/mcrute/golib/echo/controller"
15 "github.com/labstack/echo/v4"
16)
17
18type APIAccountHandler struct {
19 Store models.AccountStore
20 AdminStore models.AccountStore
21}
22
23func (h *APIAccountHandler) Register(prefix string, r glecho.URLRouter, mw ...echo.MiddlewareFunc) {
24 // This resource did not exist in the V1 API and thus has no V1
25 // representation. We use the default handlers for V1 because otherwise
26 // requests with V1 Accept headers would result in 406 Unacceptable errors.
27 gh := &controller.ContentTypeNegotiatingHandler{
28 DefaultHandler: h.HandleGet,
29 Handlers: map[string]echo.HandlerFunc{
30 contentTypeV1: h.HandleGet,
31 contentTypeV2: h.HandleGet,
32 },
33 }
34 r.GET(prefix, gh.Handle, mw...)
35
36 ph := &controller.ContentTypeNegotiatingHandler{
37 DefaultHandler: h.HandlePut,
38 Handlers: map[string]echo.HandlerFunc{
39 contentTypeV1: h.HandlePut,
40 contentTypeV2: h.HandlePut,
41 },
42 }
43 r.PUT(prefix, ph.Handle, mw...)
44
45 poh := &controller.ContentTypeNegotiatingHandler{
46 DefaultHandler: h.HandlePost,
47 Handlers: map[string]echo.HandlerFunc{
48 contentTypeV1: h.HandlePost,
49 contentTypeV2: h.HandlePost,
50 },
51 }
52 r.POST(prefix, poh.Handle, mw...)
53
54 r.DELETE(prefix, h.HandleDelete, mw...)
55}
56
57func (h *APIAccountHandler) getPrincipalAndAccount(c echo.Context) (*models.User, *models.Account, error) {
58 var err error
59 ctx := context.Background()
60
61 p, err := middleware.GetAuthorizedPrincipal(c)
62 if err != nil {
63 return nil, nil, echo.ErrUnauthorized
64 }
65
66 var a *models.Account
67 if p.IsAdmin {
68 a, err = h.AdminStore.GetForUser(ctx, c.Param("account"), p)
69 if err != nil {
70 return nil, nil, echo.NotFoundHandler(c)
71 }
72 } else {
73 a, err = h.Store.GetForUser(ctx, c.Param("account"), p)
74 if err != nil {
75 return nil, nil, echo.NotFoundHandler(c)
76 }
77 }
78
79 return p, a, nil
80}
81
82func (h *APIAccountHandler) HandleGet(c echo.Context) error {
83 p, a, err := h.getPrincipalAndAccount(c)
84 if err != nil {
85 return err
86 }
87
88 // These fields are slightly sensitive and give away too many security
89 // details about the account so they should only be visible to users who
90 // can administer the account.
91 if !a.CanBeModifiedBy(p) {
92 a.VaultMaterial = ""
93 a.Users = nil
94 }
95
96 return c.JSON(http.StatusOK, a)
97}
98
99func (h *APIAccountHandler) HandlePut(c echo.Context) error {
100 var in models.Account
101 if err := c.Echo().JSONSerializer.Deserialize(c, &in); err != nil {
102 return echo.ErrBadRequest
103 }
104
105 p, a, err := h.getPrincipalAndAccount(c)
106 if err != nil {
107 return err
108 }
109
110 if !a.CanBeModifiedBy(p) {
111 return echo.ErrForbidden
112 }
113
114 if in.ShortName != a.ShortName {
115 return &echo.HTTPError{
116 Code: http.StatusBadRequest,
117 Message: "Account short_name can not be changed. Create a new account.",
118 }
119 }
120
121 if in.AccountType != a.AccountType {
122 return &echo.HTTPError{
123 Code: http.StatusBadRequest,
124 Message: "Account type can not be changed. Create a new account.",
125 }
126 }
127
128 a.AccountNumber = in.AccountNumber
129 a.Name = in.Name
130 a.ConsoleSessionDuration = in.ConsoleSessionDuration
131 a.VaultMaterial = in.VaultMaterial
132 a.DefaultRegion = in.DefaultRegion
133 a.Users = in.Users
134
135 // PUT-ing Deleted equal to null effectively un-deletes the record
136 a.Deleted = in.Deleted
137
138 if err := h.Store.Put(context.Background(), a); err != nil {
139 return echo.ErrInternalServerError
140 }
141
142 return c.String(http.StatusNoContent, "")
143}
144
145func (h *APIAccountHandler) HandlePost(c echo.Context) error {
146 var in models.Account
147 if err := c.Echo().JSONSerializer.Deserialize(c, &in); err != nil {
148 return echo.ErrBadRequest
149 }
150
151 if _, err := h.AdminStore.Get(context.Background(), in.ShortName); err == nil {
152 return &echo.HTTPError{
153 Code: http.StatusConflict,
154 Message: "Account with short name already exists. Choose another short name.",
155 }
156 }
157
158 if in.ConsoleSessionDuration < time.Hour {
159 in.ConsoleSessionDuration = time.Hour
160 }
161
162 if in.ConsoleSessionDuration > 12*time.Hour {
163 return &echo.HTTPError{
164 Code: http.StatusBadRequest,
165 Message: "Console duration is greater than the AWS maximum of 12 hours.",
166 }
167 }
168
169 if in.Deleted != nil {
170 return &echo.HTTPError{
171 Code: http.StatusBadRequest,
172 Message: "Can not create deleted account, set Deleted to null",
173 }
174 }
175
176 if err := aws.ValidateVaultMaterial(in.VaultMaterial); err != nil {
177 return &echo.HTTPError{
178 Code: http.StatusBadRequest,
179 Message: fmt.Sprintf("Unable to access Vault material: %s", err),
180 }
181 }
182
183 if err := h.Store.Put(context.Background(), &in); err != nil {
184 return echo.ErrInternalServerError
185 }
186
187 c.Response().Header().Add("Location", glecho.URLFor(c, "/api/account", in.ShortName).String())
188
189 return c.String(http.StatusCreated, "")
190}
191
192func (h *APIAccountHandler) HandleDelete(c echo.Context) error {
193 p, a, err := h.getPrincipalAndAccount(c)
194 if err != nil {
195 return err
196 }
197
198 if a.CanBeModifiedBy(p) {
199 if err := h.Store.Delete(context.Background(), a); err != nil {
200 return echo.ErrInternalServerError
201 }
202 } else {
203 return echo.ErrForbidden
204 }
205
206 return c.String(http.StatusNoContent, "")
207}
diff --git a/app/controllers/api_account_list.go b/app/controllers/api_account_list.go
index f69db6a..28b64c1 100644
--- a/app/controllers/api_account_list.go
+++ b/app/controllers/api_account_list.go
@@ -17,6 +17,7 @@ type jsonAccount struct {
17 AccountNumber int `json:"account_number"` 17 AccountNumber int `json:"account_number"`
18 ShortName string `json:"short_name"` 18 ShortName string `json:"short_name"`
19 Name string `json:"name"` 19 Name string `json:"name"`
20 SelfUrl string `json:"url"`
20 ConsoleUrl string `json:"get_console_url,omitempty"` 21 ConsoleUrl string `json:"get_console_url,omitempty"`
21 ConsoleRedirectUrl string `json:"console_redirect_url,omitempty"` 22 ConsoleRedirectUrl string `json:"console_redirect_url,omitempty"`
22 CredentialsUrl string `json:"credentials_url"` 23 CredentialsUrl string `json:"credentials_url"`
@@ -28,6 +29,7 @@ func jsonAccountFromAccount(c echo.Context, a *models.Account) *jsonAccount {
28 AccountNumber: a.AccountNumber, 29 AccountNumber: a.AccountNumber,
29 ShortName: a.ShortName, 30 ShortName: a.ShortName,
30 Name: a.Name, 31 Name: a.Name,
32 SelfUrl: glecho.URLFor(c, "/api/account", a.ShortName).String(),
31 ConsoleUrl: glecho.URLFor(c, "/api/account", a.ShortName, "console").String(), 33 ConsoleUrl: glecho.URLFor(c, "/api/account", a.ShortName, "console").String(),
32 ConsoleRedirectUrl: glecho.URLFor(c, "/api/account", a.ShortName, "console").Query("redirect", "1").String(), 34 ConsoleRedirectUrl: glecho.URLFor(c, "/api/account", a.ShortName, "console").Query("redirect", "1").String(),
33 CredentialsUrl: glecho.URLFor(c, "/api/account", a.ShortName, "credentials").String(), 35 CredentialsUrl: glecho.URLFor(c, "/api/account", a.ShortName, "credentials").String(),
diff --git a/app/models/account.go b/app/models/account.go
index 0ae1821..61b144d 100644
--- a/app/models/account.go
+++ b/app/models/account.go
@@ -2,10 +2,11 @@ package models
2 2
3import ( 3import (
4 "context" 4 "context"
5 "fmt"
6 "time" 5 "time"
7 6
8 "code.crute.us/mcrute/golib/db/mongodb" 7 "code.crute.us/mcrute/golib/db/mongodb"
8 "go.mongodb.org/mongo-driver/bson"
9 "go.mongodb.org/mongo-driver/bson/primitive"
9) 10)
10 11
11const accountCol = "accounts" 12const accountCol = "accounts"
@@ -20,43 +21,48 @@ type AccountStore interface {
20} 21}
21 22
22type Account struct { 23type Account struct {
23 ShortName string `bson:"_id"` 24 ShortName string `bson:"_id" json:"short_name"`
24 AccountType string 25 AccountType string `json:"account_type"`
25 AccountNumber int 26 AccountNumber int `json:"account_number"`
26 Name string 27 Name string `json:"name"`
27 ConsoleSessionDuration time.Duration 28 ConsoleSessionDuration time.Duration `json:"console_session_duration, omitempty"`
28 VaultMaterial string 29 VaultMaterial string `json:"vault_material,omitempty"`
29 DefaultRegion string 30 DefaultRegion string `json:"default_region"`
30 Users []string 31 Users []string `json:"users,omitempty"`
32 Deleted *time.Time `json:"deleted,omitempty" bson:"deleted,omitempty"`
31} 33}
32 34
33func (a *Account) ConsoleSessionDurationSecs() int64 { 35func (a *Account) ConsoleSessionDurationSecs() int64 {
34 return int64(a.ConsoleSessionDuration.Seconds()) 36 return int64(a.ConsoleSessionDuration.Seconds())
35} 37}
36 38
37func (a *Account) CanAccess(u *User) bool { 39func (a *Account) CanBeModifiedBy(u *User) bool {
38 if u.IsAdmin { 40 return u.IsAdmin
39 return true
40 }
41 // Linear search should be fine for now, these lists are pretty small
42 for _, n := range a.Users {
43 if n == u.Username {
44 return true
45 }
46 }
47 return false
48} 41}
49 42
50type MongoDbAccountStore struct { 43type MongoDbAccountStore struct {
51 Db *mongodb.Mongo 44 Db *mongodb.Mongo
45
46 // ReturnDeleted will allow all methods to return deleted items. By default
47 // items where the Deleted field is set will not be returned. This should
48 // be the common cast for most code using this store but in some Admin
49 // use-cases it would be useful to show deleted accounts.
50 ReturnDeleted bool
52} 51}
53 52
54// List returns all accounts in the system. 53// List returns all accounts in the system.
55func (s *MongoDbAccountStore) List(ctx context.Context) ([]*Account, error) { 54func (s *MongoDbAccountStore) List(ctx context.Context) ([]*Account, error) {
56 var out []*Account 55 var out []*Account
57 if err := s.Db.FindAll(ctx, accountCol, &out); err != nil { 56
57 filter := bson.M{}
58 if !s.ReturnDeleted {
59 filter["deleted"] = primitive.Null{}
60 }
61
62 if err := s.Db.FindAllByFilter(ctx, accountCol, filter, &out); err != nil {
58 return nil, err 63 return nil, err
59 } 64 }
65
60 return out, nil 66 return out, nil
61} 67}
62 68
@@ -68,34 +74,56 @@ func (s *MongoDbAccountStore) List(ctx context.Context) ([]*Account, error) {
68// just use List directly. 74// just use List directly.
69func (s *MongoDbAccountStore) ListForUser(ctx context.Context, u *User) ([]*Account, error) { 75func (s *MongoDbAccountStore) ListForUser(ctx context.Context, u *User) ([]*Account, error) {
70 var out []*Account 76 var out []*Account
71 filter := mongodb.AnyInTopLevelArray("Users", u.Username) 77
78 filter := mongodb.AnyInTopLevelArray("users", u.Username)
79 if !s.ReturnDeleted {
80 filter["deleted"] = primitive.Null{}
81 }
82
72 if err := s.Db.FindAllByFilter(ctx, accountCol, filter, &out); err != nil { 83 if err := s.Db.FindAllByFilter(ctx, accountCol, filter, &out); err != nil {
73 return nil, err 84 return nil, err
74 } 85 }
86
75 return out, nil 87 return out, nil
76} 88}
77 89
78func (s *MongoDbAccountStore) Get(ctx context.Context, id string) (*Account, error) { 90func (s *MongoDbAccountStore) Get(ctx context.Context, id string) (*Account, error) {
79 var a Account 91 var a Account
80 if err := s.Db.FindOneById(ctx, accountCol, id, &a); err != nil { 92
93 filter := bson.M{"_id": id}
94 if !s.ReturnDeleted {
95 filter["deleted"] = primitive.Null{}
96 }
97
98 if err := s.Db.FindOneByFilter(ctx, accountCol, filter, &a); err != nil {
81 return nil, err 99 return nil, err
82 } 100 }
101
83 return &a, nil 102 return &a, nil
84} 103}
85 104
86// GetForUser returns an account if the user has access to this account, 105// GetForUser returns an account if the user has access to this account,
87// otherwise it returns an error. This is the authorized version of Get. 106// otherwise it returns an error. This is the authorized version of Get.
88func (s *MongoDbAccountStore) GetForUser(ctx context.Context, id string, u *User) (*Account, error) { 107func (s *MongoDbAccountStore) GetForUser(ctx context.Context, id string, u *User) (*Account, error) {
89 a, err := s.Get(ctx, id) 108 var a Account
90 if err != nil { 109 var filter bson.M
91 return nil, err 110
111 if u.IsAdmin {
112 filter = bson.M{"_id": id}
113 } else {
114 filter = mongodb.AnyInTopLevelArray("users", u.Username)
115 filter["_id"] = id
92 } 116 }
93 117
94 if !a.CanAccess(u) { 118 if !s.ReturnDeleted {
95 return nil, fmt.Errorf("User does not have access to account") 119 filter["deleted"] = primitive.Null{}
96 } 120 }
97 121
98 return a, nil 122 if err := s.Db.FindOneByFilter(ctx, accountCol, filter, &a); err != nil {
123 return nil, err
124 }
125
126 return &a, nil
99} 127}
100 128
101func (s *MongoDbAccountStore) Put(ctx context.Context, a *Account) error { 129func (s *MongoDbAccountStore) Put(ctx context.Context, a *Account) error {
@@ -106,10 +134,15 @@ func (s *MongoDbAccountStore) Put(ctx context.Context, a *Account) error {
106} 134}
107 135
108func (s *MongoDbAccountStore) Delete(ctx context.Context, a *Account) error { 136func (s *MongoDbAccountStore) Delete(ctx context.Context, a *Account) error {
109 if err := s.Db.DeleteOneById(ctx, accountCol, a.ShortName); err != nil { 137 a, err := s.Get(ctx, a.ShortName)
138 if err != nil {
110 return err 139 return err
111 } 140 }
112 return nil 141
142 now := time.Now()
143 a.Deleted = &now
144
145 return s.Put(ctx, a)
113} 146}
114 147
115var _ AccountStore = (*MongoDbAccountStore)(nil) 148var _ AccountStore = (*MongoDbAccountStore)(nil)