aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2021-11-22 18:42:45 -0800
committerMike Crute <mike@crute.us>2021-11-22 18:42:45 -0800
commit853cab121191a2cf4dd37c68149fc23b64235464 (patch)
treeeb8e0ceb21daa457e7bd98e12379c6706379c26c
parent22819ad3543b6bad4f6efcedbebb8437292cae3b (diff)
downloadcloud-identity-broker-853cab121191a2cf4dd37c68149fc23b64235464.tar.bz2
cloud-identity-broker-853cab121191a2cf4dd37c68149fc23b64235464.tar.xz
cloud-identity-broker-853cab121191a2cf4dd37c68149fc23b64235464.zip
Add user endpoints
-rw-r--r--app/controllers/api_user.go181
-rw-r--r--app/controllers/api_user_list.go56
-rw-r--r--app/middleware/perms_check.go20
-rw-r--r--app/models/user.go56
-rw-r--r--cmd/web/server.go14
5 files changed, 315 insertions, 12 deletions
diff --git a/app/controllers/api_user.go b/app/controllers/api_user.go
new file mode 100644
index 0000000..df667db
--- /dev/null
+++ b/app/controllers/api_user.go
@@ -0,0 +1,181 @@
1package controllers
2
3import (
4 "context"
5 "net/http"
6
7 "code.crute.us/mcrute/cloud-identity-broker/app/models"
8
9 glecho "code.crute.us/mcrute/golib/echo"
10 "code.crute.us/mcrute/golib/echo/controller"
11 "github.com/labstack/echo/v4"
12)
13
14type APIUserHandler struct {
15 Store models.UserStore
16}
17
18func (h *APIUserHandler) Register(prefix string, r glecho.URLRouter, mw ...echo.MiddlewareFunc) {
19 // This resource did not exist in the V1 API and thus has no V1
20 // representation. We use the default handlers for V1 because otherwise
21 // requests with V1 Accept headers would result in 406 Unacceptable errors.
22 gh := &controller.ContentTypeNegotiatingHandler{
23 DefaultHandler: h.HandleGet,
24 Handlers: map[string]echo.HandlerFunc{
25 contentTypeV1: h.HandleGet,
26 contentTypeV2: h.HandleGet,
27 },
28 }
29 r.GET(prefix, gh.Handle, mw...)
30
31 ph := &controller.ContentTypeNegotiatingHandler{
32 DefaultHandler: h.HandlePut,
33 Handlers: map[string]echo.HandlerFunc{
34 contentTypeV1: h.HandlePut,
35 contentTypeV2: h.HandlePut,
36 },
37 }
38 r.PUT(prefix, ph.Handle, mw...)
39
40 poh := &controller.ContentTypeNegotiatingHandler{
41 DefaultHandler: h.HandlePost,
42 Handlers: map[string]echo.HandlerFunc{
43 contentTypeV1: h.HandlePost,
44 contentTypeV2: h.HandlePost,
45 },
46 }
47 r.POST(prefix, poh.Handle, mw...)
48
49 r.DELETE(prefix, h.HandleDelete, mw...)
50}
51
52func (h *APIUserHandler) HandleGet(c echo.Context) error {
53 u, err := h.Store.Get(context.Background(), c.Param("user"))
54 if err != nil {
55 return echo.ErrInternalServerError
56 }
57
58 return c.JSON(http.StatusOK, u)
59}
60
61func validateKeysAndTokens(in *models.User) error {
62 for k, v := range in.Keys {
63 if k != v.KeyId {
64 return &echo.HTTPError{
65 Code: http.StatusBadRequest,
66 Message: "Key ID must match hash key.",
67 }
68 }
69
70 if v.PrivateKey == nil && v.PublicKey == nil {
71 return &echo.HTTPError{
72 Code: http.StatusBadRequest,
73 Message: "One of public_key or private_key must be set",
74 }
75 }
76
77 if v.PrivateKey != nil && v.PublicKey != nil {
78 return &echo.HTTPError{
79 Code: http.StatusBadRequest,
80 Message: "Only one of public_key or private_key may be set",
81 }
82 }
83 }
84
85 for k, v := range in.AuthTokens {
86 if k != v.Kind {
87 return &echo.HTTPError{
88 Code: http.StatusBadRequest,
89 Message: "Token kind must match hash key.",
90 }
91 }
92 }
93
94 return nil
95}
96
97func (h *APIUserHandler) HandlePut(c echo.Context) error {
98 var in models.User
99 if err := c.Echo().JSONSerializer.Deserialize(c, &in); err != nil {
100 return echo.ErrBadRequest
101 }
102
103 u, err := h.Store.Get(context.Background(), c.Param("user"))
104 if err != nil {
105 return echo.ErrInternalServerError
106 }
107
108 if in.Username != u.Username {
109 return &echo.HTTPError{
110 Code: http.StatusBadRequest,
111 Message: "Username can not be changed. Create a new user.",
112 }
113 }
114
115 if in.Deleted != nil && u.Deleted == nil {
116 return &echo.HTTPError{
117 Code: http.StatusBadRequest,
118 Message: "Use the DELETE method to delete a record",
119 }
120 }
121
122 if err := validateKeysAndTokens(&in); err != nil {
123 return err
124 }
125
126 err = h.Store.Put(context.Background(), &in)
127 if err != nil {
128 return echo.ErrInternalServerError
129 }
130
131 return c.String(http.StatusNoContent, "")
132}
133
134func (h *APIUserHandler) HandlePost(c echo.Context) error {
135 var in models.User
136 if err := c.Echo().JSONSerializer.Deserialize(c, &in); err != nil {
137 return echo.ErrBadRequest
138 }
139
140 _, err := h.Store.Get(context.Background(), c.Param("user"))
141 if err == nil {
142 return &echo.HTTPError{
143 Code: http.StatusConflict,
144 Message: "User with username already exists.",
145 }
146 }
147
148 if in.Deleted != nil {
149 return &echo.HTTPError{
150 Code: http.StatusBadRequest,
151 Message: "Can not create deleted user, set Deleted to null",
152 }
153 }
154
155 if err := validateKeysAndTokens(&in); err != nil {
156 return err
157 }
158
159 err = h.Store.Put(context.Background(), &in)
160 if err != nil {
161 return echo.ErrInternalServerError
162 }
163
164 c.Response().Header().Add("Location", glecho.URLFor(c, "/api/user", in.Username).String())
165
166 return c.String(http.StatusCreated, "")
167}
168
169func (h *APIUserHandler) HandleDelete(c echo.Context) error {
170 u, err := h.Store.Get(context.Background(), c.Param("user"))
171 if err != nil {
172 return echo.ErrInternalServerError
173 }
174
175 err = h.Store.Delete(context.Background(), u)
176 if err != nil {
177 return echo.ErrInternalServerError
178 }
179
180 return c.String(http.StatusNoContent, "")
181}
diff --git a/app/controllers/api_user_list.go b/app/controllers/api_user_list.go
new file mode 100644
index 0000000..ba6dff5
--- /dev/null
+++ b/app/controllers/api_user_list.go
@@ -0,0 +1,56 @@
1package controllers
2
3import (
4 "context"
5 "net/http"
6 "time"
7
8 "code.crute.us/mcrute/cloud-identity-broker/app/models"
9
10 glecho "code.crute.us/mcrute/golib/echo"
11 "code.crute.us/mcrute/golib/echo/controller"
12 "github.com/labstack/echo/v4"
13)
14
15type jsonListUser struct {
16 Username string `bson:"_id" json:"key_id"`
17 IsAdmin bool `json:"is_admin"`
18 IsService bool `json:"is_service"`
19 SelfLink string `json:"self"`
20 Deleted *time.Time `json:"deleted,omitempty"`
21}
22
23type APIUserListHandler struct {
24 store models.UserStore
25}
26
27func NewAPIUserListHandler(s models.UserStore) echo.HandlerFunc {
28 al := &APIUserListHandler{store: s}
29 h := &controller.ContentTypeNegotiatingHandler{
30 DefaultHandler: al.Handle,
31 Handlers: map[string]echo.HandlerFunc{
32 contentTypeV2: al.Handle,
33 },
34 }
35 return h.Handle
36}
37
38func (h *APIUserListHandler) Handle(c echo.Context) error {
39 users, err := h.store.List(context.Background())
40 if err != nil {
41 return echo.ErrInternalServerError
42 }
43
44 out := map[string]*jsonListUser{}
45 for _, v := range users {
46 out[v.Username] = &jsonListUser{
47 Username: v.Username,
48 IsAdmin: v.IsAdmin,
49 IsService: v.IsService,
50 SelfLink: glecho.URLFor(c, "/api/user", v.Username).String(),
51 Deleted: v.Deleted,
52 }
53 }
54
55 return c.JSON(http.StatusOK, out)
56}
diff --git a/app/middleware/perms_check.go b/app/middleware/perms_check.go
new file mode 100644
index 0000000..d118f5a
--- /dev/null
+++ b/app/middleware/perms_check.go
@@ -0,0 +1,20 @@
1package middleware
2
3import (
4 "github.com/labstack/echo/v4"
5)
6
7func RequireAdminPrivileges(next echo.HandlerFunc) echo.HandlerFunc {
8 return func(c echo.Context) error {
9 p, err := GetAuthorizedPrincipal(c)
10 if err != nil {
11 return echo.ErrUnauthorized
12 }
13
14 if !p.IsAdmin {
15 return echo.NotFoundHandler(c)
16 }
17
18 return next(c)
19 }
20}
diff --git a/app/models/user.go b/app/models/user.go
index 0cbd92d..2871380 100644
--- a/app/models/user.go
+++ b/app/models/user.go
@@ -2,8 +2,11 @@ package models
2 2
3import ( 3import (
4 "context" 4 "context"
5 "time"
5 6
6 "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"
7) 10)
8 11
9const userCol = "users" 12const userCol = "users"
@@ -16,17 +19,21 @@ type UserStore interface {
16} 19}
17 20
18type AuthToken struct { 21type AuthToken struct {
19 Kind string 22 Kind string `json:"kind"`
20 Token string 23 Token string `json:"token"`
21 RefreshToken string 24
25 // Do not expose refresh tokens in JSON as they are long-lived tokens that
26 // are harder to invalidate and thus rather security sensitive.
27 RefreshToken string `json:"-"`
22} 28}
23 29
24type User struct { 30type User struct {
25 Username string `bson:"_id"` 31 Username string `bson:"_id" json:"key_id"`
26 IsAdmin bool 32 IsAdmin bool `json:"is_admin"`
27 IsService bool 33 IsService bool `json:"is_service"`
28 Keys map[string]*SessionKey // kid -> key 34 Keys map[string]*SessionKey `json:"keys,omitempty"` // kid -> key
29 AuthTokens map[string]*AuthToken // kind -> token 35 AuthTokens map[string]*AuthToken `json:"auth_tokens,omitempty"` // kind -> token
36 Deleted *time.Time `json:"deleted,omitempty"`
30} 37}
31 38
32// GCKeys garbage collects keys that are no longer valid 39// GCKeys garbage collects keys that are no longer valid
@@ -64,21 +71,41 @@ func (u *User) AddToken(t *AuthToken) {
64 71
65type MongoDbUserStore struct { 72type MongoDbUserStore struct {
66 Db *mongodb.Mongo 73 Db *mongodb.Mongo
74
75 // ReturnDeleted will allow all methods to return deleted items. By default
76 // items where the Deleted field is set will not be returned. This should
77 // be the common cast for most code using this store but in some Admin
78 // use-cases it would be useful to show deleted accounts.
79 ReturnDeleted bool
67} 80}
68 81
69func (s *MongoDbUserStore) List(ctx context.Context) ([]*User, error) { 82func (s *MongoDbUserStore) List(ctx context.Context) ([]*User, error) {
70 var out []*User 83 var out []*User
71 if err := s.Db.FindAll(ctx, userCol, &out); err != nil { 84
85 filter := bson.M{}
86 if !s.ReturnDeleted {
87 filter["deleted"] = primitive.Null{}
88 }
89
90 if err := s.Db.FindAllByFilter(ctx, userCol, filter, &out); err != nil {
72 return nil, err 91 return nil, err
73 } 92 }
93
74 return out, nil 94 return out, nil
75} 95}
76 96
77func (s *MongoDbUserStore) Get(ctx context.Context, username string) (*User, error) { 97func (s *MongoDbUserStore) Get(ctx context.Context, username string) (*User, error) {
78 var u User 98 var u User
79 if err := s.Db.FindOneById(ctx, userCol, username, &u); err != nil { 99
100 filter := bson.M{"_id": username}
101 if !s.ReturnDeleted {
102 filter["deleted"] = primitive.Null{}
103 }
104
105 if err := s.Db.FindOneByFilter(ctx, userCol, filter, &u); err != nil {
80 return nil, err 106 return nil, err
81 } 107 }
108
82 return &u, nil 109 return &u, nil
83} 110}
84 111
@@ -90,10 +117,15 @@ func (s *MongoDbUserStore) Put(ctx context.Context, u *User) error {
90} 117}
91 118
92func (s *MongoDbUserStore) Delete(ctx context.Context, u *User) error { 119func (s *MongoDbUserStore) Delete(ctx context.Context, u *User) error {
93 if err := s.Db.DeleteOneById(ctx, userCol, u.Username); err != nil { 120 u, err := s.Get(ctx, u.Username)
121 if err != nil {
94 return err 122 return err
95 } 123 }
96 return nil 124
125 now := time.Now()
126 u.Deleted = &now
127
128 return s.Put(ctx, u)
97} 129}
98 130
99var _ UserStore = (*MongoDbUserStore)(nil) 131var _ UserStore = (*MongoDbUserStore)(nil)
diff --git a/cmd/web/server.go b/cmd/web/server.go
index c573244..5b2e025 100644
--- a/cmd/web/server.go
+++ b/cmd/web/server.go
@@ -108,6 +108,11 @@ func webMain(cfg app.Config, embeddedTemplates fs.FS, version string) {
108 ReturnDeleted: true, 108 ReturnDeleted: true,
109 } 109 }
110 as := &models.MongoDbAccountStore{Db: mongo} 110 as := &models.MongoDbAccountStore{Db: mongo}
111
112 adminUserStore := &models.MongoDbUserStore{
113 Db: mongo,
114 ReturnDeleted: true,
115 }
111 us := &models.MongoDbUserStore{Db: mongo} 116 us := &models.MongoDbUserStore{Db: mongo}
112 117
113 aws := &controllers.AWSAPI{Store: as} 118 aws := &controllers.AWSAPI{Store: as}
@@ -156,6 +161,15 @@ func webMain(cfg app.Config, embeddedTemplates fs.FS, version string) {
156 AdminStore: adminAccountStore, 161 AdminStore: adminAccountStore,
157 }).Register("/:account", account) 162 }).Register("/:account", account)
158 } 163 }
164
165 user := api.Group("/user")
166 user.Use(middleware.RequireAdminPrivileges)
167 {
168 user.GET("", controllers.NewAPIUserListHandler(us))
169 (&controllers.APIUserHandler{
170 Store: adminUserStore,
171 }).Register("/:user", user)
172 }
159 } 173 }
160 s.GET("/favicon.ico", echo.NotFoundHandler) 174 s.GET("/favicon.ico", echo.NotFoundHandler)
161 s.GET("/logout", controllers.LogoutHandler) 175 s.GET("/logout", controllers.LogoutHandler)