Skip to content

Commit c45f21b

Browse files
Feat: API Keys CRUD and use it for authentication and authorisation (#1782)
* wip * wip2 * wip * wire_gen * bug fixes * api token integration * lastUsedAt and lastUsedByIp updated in user for api-user case * bug fix * deleted * renamed * fix : Name should be reusable if API-token is deleted * code comments incorporate : 1) added validation : name can not have whitespace and comma while creating api-token 2) api-token email prefix capital 3) moved RBAC before validations in rest handler of api-token 4) user audit table * code comment incorporate : 1) using attributes table/service/repo to store secret * wiring fix * fix * sql file number changed * some fields added in crud apis
1 parent 584c2f1 commit c45f21b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3954
-64
lines changed

Wire.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package main
2222

2323
import (
24+
"github.com/devtron-labs/devtron/api/apiToken"
2425
appStoreRestHandler "github.com/devtron-labs/devtron/api/appStore"
2526
appStoreDeployment "github.com/devtron-labs/devtron/api/appStore/deployment"
2627
appStoreDiscover "github.com/devtron-labs/devtron/api/appStore/discover"
@@ -123,6 +124,7 @@ func InitializeApp() (*App, error) {
123124
appStoreDeployment.AppStoreDeploymentWireSet,
124125
server.ServerWireSet,
125126
module.ModuleWireSet,
127+
apiToken.ApiTokenWireSet,
126128
// -------wireset end ----------
127129
gitSensor.GetGitSensorConfig,
128130
gitSensor.NewGitSensorSession,
@@ -600,6 +602,7 @@ func InitializeApp() (*App, error) {
600602
wire.Bind(new(router.CommonRouter), new(*router.CommonRouterImpl)),
601603
restHandler.NewCommonRestHanlderImpl,
602604
wire.Bind(new(restHandler.CommonRestHanlder), new(*restHandler.CommonRestHanlderImpl)),
605+
603606
util.NewGitCliUtil,
604607

605608
router.NewTelemetryRouterImpl,
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* Copyright (c) 2020 Devtron Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package apiToken
19+
20+
import (
21+
"encoding/json"
22+
openapi "github.com/devtron-labs/devtron/api/openapi/openapiClient"
23+
"github.com/devtron-labs/devtron/api/restHandler/common"
24+
"github.com/devtron-labs/devtron/pkg/apiToken"
25+
"github.com/devtron-labs/devtron/pkg/user"
26+
"github.com/devtron-labs/devtron/pkg/user/casbin"
27+
"github.com/gorilla/mux"
28+
"github.com/juju/errors"
29+
"go.uber.org/zap"
30+
"gopkg.in/go-playground/validator.v9"
31+
"net/http"
32+
"strconv"
33+
)
34+
35+
type ApiTokenRestHandler interface {
36+
GetAllApiTokens(w http.ResponseWriter, r *http.Request)
37+
CreateApiToken(w http.ResponseWriter, r *http.Request)
38+
UpdateApiToken(w http.ResponseWriter, r *http.Request)
39+
DeleteApiToken(w http.ResponseWriter, r *http.Request)
40+
}
41+
42+
type ApiTokenRestHandlerImpl struct {
43+
logger *zap.SugaredLogger
44+
apiTokenService apiToken.ApiTokenService
45+
userService user.UserService
46+
enforcer casbin.Enforcer
47+
validator *validator.Validate
48+
}
49+
50+
func NewApiTokenRestHandlerImpl(logger *zap.SugaredLogger, apiTokenService apiToken.ApiTokenService, userService user.UserService,
51+
enforcer casbin.Enforcer, validator *validator.Validate) *ApiTokenRestHandlerImpl {
52+
return &ApiTokenRestHandlerImpl{
53+
logger: logger,
54+
apiTokenService: apiTokenService,
55+
userService: userService,
56+
enforcer: enforcer,
57+
validator: validator,
58+
}
59+
}
60+
61+
func (impl ApiTokenRestHandlerImpl) GetAllApiTokens(w http.ResponseWriter, r *http.Request) {
62+
userId, err := impl.userService.GetLoggedInUser(r)
63+
if userId == 0 || err != nil {
64+
common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized)
65+
return
66+
}
67+
68+
// handle super-admin RBAC
69+
token := r.Header.Get("token")
70+
if ok := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*"); !ok {
71+
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
72+
return
73+
}
74+
75+
// service call
76+
res, err := impl.apiTokenService.GetAllActiveApiTokens()
77+
if err != nil {
78+
impl.logger.Errorw("service err, GetAllActiveApiTokens", "err", err)
79+
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
80+
return
81+
}
82+
common.WriteJsonResp(w, err, res, http.StatusOK)
83+
}
84+
85+
func (impl ApiTokenRestHandlerImpl) CreateApiToken(w http.ResponseWriter, r *http.Request) {
86+
userId, err := impl.userService.GetLoggedInUser(r)
87+
if userId == 0 || err != nil {
88+
common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized)
89+
return
90+
}
91+
92+
// handle super-admin RBAC
93+
token := r.Header.Get("token")
94+
if ok := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*"); !ok {
95+
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
96+
return
97+
}
98+
99+
// decode request
100+
decoder := json.NewDecoder(r.Body)
101+
var request *openapi.CreateApiTokenRequest
102+
err = decoder.Decode(&request)
103+
if err != nil {
104+
impl.logger.Errorw("err in decoding request in CreateApiToken", "err", err)
105+
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
106+
return
107+
}
108+
109+
// validate request
110+
err = impl.validator.Struct(request)
111+
if err != nil {
112+
impl.logger.Errorw("validation err in CreateApiToken", "err", err, "request", request)
113+
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
114+
return
115+
}
116+
if len(*request.Name) == 0 {
117+
common.WriteJsonResp(w, errors.New("name cannot be blank in the request"), nil, http.StatusBadRequest)
118+
return
119+
}
120+
if len(*request.Description) == 0 {
121+
common.WriteJsonResp(w, errors.New("description cannot be blank in the request"), nil, http.StatusBadRequest)
122+
return
123+
}
124+
125+
// service call
126+
res, err := impl.apiTokenService.CreateApiToken(request, userId)
127+
if err != nil {
128+
impl.logger.Errorw("service err, CreateApiToken", "err", err, "payload", request)
129+
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
130+
return
131+
}
132+
common.WriteJsonResp(w, err, res, http.StatusOK)
133+
}
134+
135+
func (impl ApiTokenRestHandlerImpl) UpdateApiToken(w http.ResponseWriter, r *http.Request) {
136+
userId, err := impl.userService.GetLoggedInUser(r)
137+
if userId == 0 || err != nil {
138+
common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized)
139+
return
140+
}
141+
142+
// handle super-admin RBAC
143+
token := r.Header.Get("token")
144+
if ok := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*"); !ok {
145+
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
146+
return
147+
}
148+
149+
// get api-token Id
150+
vars := mux.Vars(r)
151+
apiTokenId, err := strconv.Atoi(vars["id"])
152+
if err != nil {
153+
impl.logger.Errorw("request err in getting apiTokenId in UpdateApiToken", "err", err)
154+
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
155+
return
156+
}
157+
158+
// decode request
159+
decoder := json.NewDecoder(r.Body)
160+
var request *openapi.UpdateApiTokenRequest
161+
err = decoder.Decode(&request)
162+
if err != nil {
163+
impl.logger.Errorw("err in decoding request, UpdateApiToken", "err", err)
164+
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
165+
return
166+
}
167+
168+
// validate request
169+
err = impl.validator.Struct(request)
170+
if err != nil {
171+
impl.logger.Errorw("validation err in UpdateApiToken", "err", err, "request", request)
172+
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
173+
return
174+
}
175+
if len(*request.Description) == 0 {
176+
common.WriteJsonResp(w, errors.New("description cannot be blank in the request"), nil, http.StatusBadRequest)
177+
return
178+
}
179+
180+
res, err := impl.apiTokenService.UpdateApiToken(apiTokenId, request, userId)
181+
if err != nil {
182+
impl.logger.Errorw("service err, UpdateApiToken", "err", err, "apiTokenId", apiTokenId, "request", request)
183+
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
184+
return
185+
}
186+
common.WriteJsonResp(w, err, res, http.StatusOK)
187+
}
188+
189+
func (impl ApiTokenRestHandlerImpl) DeleteApiToken(w http.ResponseWriter, r *http.Request) {
190+
userId, err := impl.userService.GetLoggedInUser(r)
191+
if userId == 0 || err != nil {
192+
common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized)
193+
return
194+
}
195+
196+
// handle super-admin RBAC
197+
token := r.Header.Get("token")
198+
if ok := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*"); !ok {
199+
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
200+
return
201+
}
202+
203+
// get api-token Id
204+
vars := mux.Vars(r)
205+
apiTokenId, err := strconv.Atoi(vars["id"])
206+
if err != nil {
207+
impl.logger.Errorw("request err in getting apiTokenId in DeleteApiToken", "err", err)
208+
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
209+
return
210+
}
211+
212+
res, err := impl.apiTokenService.DeleteApiToken(apiTokenId, userId)
213+
if err != nil {
214+
impl.logger.Errorw("service err, DeleteApiToken", "err", err, "apiTokenId", apiTokenId)
215+
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
216+
return
217+
}
218+
common.WriteJsonResp(w, err, res, http.StatusOK)
219+
}

api/apiToken/ApiTokenRouter.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package apiToken
2+
3+
import (
4+
"github.com/gorilla/mux"
5+
)
6+
7+
type ApiTokenRouter interface {
8+
InitApiTokenRouter(configRouter *mux.Router)
9+
}
10+
11+
type ApiTokenRouterImpl struct {
12+
apiTokenRestHandler ApiTokenRestHandler
13+
}
14+
15+
func NewApiTokenRouterImpl(apiTokenRestHandler ApiTokenRestHandler) *ApiTokenRouterImpl {
16+
return &ApiTokenRouterImpl{apiTokenRestHandler: apiTokenRestHandler}
17+
}
18+
19+
func (impl ApiTokenRouterImpl) InitApiTokenRouter(configRouter *mux.Router) {
20+
configRouter.Path("").HandlerFunc(impl.apiTokenRestHandler.GetAllApiTokens).Methods("GET")
21+
configRouter.Path("").HandlerFunc(impl.apiTokenRestHandler.CreateApiToken).Methods("POST")
22+
configRouter.Path("/{id}").HandlerFunc(impl.apiTokenRestHandler.UpdateApiToken).Methods("PUT")
23+
configRouter.Path("/{id}").HandlerFunc(impl.apiTokenRestHandler.DeleteApiToken).Methods("DELETE")
24+
}

api/apiToken/wire_apiToken.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package apiToken
2+
3+
import (
4+
"github.com/devtron-labs/devtron/pkg/apiToken"
5+
"github.com/google/wire"
6+
)
7+
8+
var ApiTokenWireSet = wire.NewSet(
9+
apiToken.NewApiTokenRepositoryImpl,
10+
wire.Bind(new(apiToken.ApiTokenRepository), new(*apiToken.ApiTokenRepositoryImpl)),
11+
apiToken.NewApiTokenServiceImpl,
12+
wire.Bind(new(apiToken.ApiTokenService), new(*apiToken.ApiTokenServiceImpl)),
13+
NewApiTokenRestHandlerImpl,
14+
wire.Bind(new(ApiTokenRestHandler), new(*ApiTokenRestHandlerImpl)),
15+
NewApiTokenRouterImpl,
16+
wire.Bind(new(ApiTokenRouter), new(*ApiTokenRouterImpl)),
17+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package apiToken
2+
3+
import (
4+
apiTokenAuth "github.com/devtron-labs/authenticator/apiToken"
5+
"github.com/devtron-labs/devtron/pkg/apiToken"
6+
"github.com/google/wire"
7+
)
8+
9+
var ApiTokenSecretWireSet = wire.NewSet(
10+
apiTokenAuth.InitApiTokenSecretStore,
11+
apiToken.NewApiTokenSecretServiceImpl,
12+
wire.Bind(new(apiToken.ApiTokenSecretService), new(*apiToken.ApiTokenSecretServiceImpl)),
13+
)

api/bean/UserRequest.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
package bean
1919

20-
import "encoding/json"
20+
import (
21+
"encoding/json"
22+
"time"
23+
)
2124

2225
type UserRole struct {
2326
Id int32 `json:"id" validate:"number"`
@@ -26,16 +29,19 @@ type UserRole struct {
2629
}
2730

2831
type UserInfo struct {
29-
Id int32 `json:"id" validate:"number"`
30-
EmailId string `json:"email_id" validate:"required"`
31-
Roles []string `json:"roles,omitempty"`
32-
AccessToken string `json:"access_token,omitempty"`
33-
Exist bool `json:"-"`
34-
UserId int32 `json:"-"` // created or modified user id
35-
RoleFilters []RoleFilter `json:"roleFilters"`
36-
Status string `json:"status,omitempty"`
37-
Groups []string `json:"groups"`
38-
SuperAdmin bool `json:"superAdmin,notnull"`
32+
Id int32 `json:"id" validate:"number"`
33+
EmailId string `json:"email_id" validate:"required"`
34+
Roles []string `json:"roles,omitempty"`
35+
AccessToken string `json:"access_token,omitempty"`
36+
UserType string `json:"-"`
37+
LastUsedAt time.Time `json:"-"`
38+
LastUsedByIp string `json:"-"`
39+
Exist bool `json:"-"`
40+
UserId int32 `json:"-"` // created or modified user id
41+
RoleFilters []RoleFilter `json:"roleFilters"`
42+
Status string `json:"status,omitempty"`
43+
Groups []string `json:"groups"`
44+
SuperAdmin bool `json:"superAdmin,notnull"`
3945
}
4046

4147
type RoleGroup struct {
@@ -95,3 +101,5 @@ const (
95101

96102
const SUPERADMIN = "role:super-admin___"
97103
const APP_ACCESS_TYPE_HELM = "helm-app"
104+
105+
const USER_TYPE_API_TOKEN = "apiToken"

0 commit comments

Comments
 (0)