Skip to content

Commit

Permalink
Implement multiple users with policies (#100)
Browse files Browse the repository at this point in the history
* Update getting-started

* [WIP] Add RBAC proposal

* [WIP] Add RBAC proposal

* Update Config struct

- Add JWT Configuration to prepare rework
authentication & authorization.
- Remove GlobalConfig.

* Init a new Auth module

* Update getting-started

* Update Config struct

- Add JWT Configuration to prepare rework
authentication & authorization.
- Remove GlobalConfig.

* Init a new Auth module

* Nit:Remove return

* WIP Init user logic

* Add bcrypt functions

* Init a new Authentication mechanism

Using jwt-auth lib [1], there are two authentication
options now:
- Cookie.
- Bearer tokens.

[1] https://github.com/adam-hanna/jwt-auth

* Update rbac proposal

* Update go.mod

* Use ntk148v/jwt-auth module

* Revert disable CSRF

* Move signout to restricted route

* Grant role with valid permissions when creating user

* Fix wrong HTTP status code in error response

HTTP status code always be 200 OK even if
there is an error response due to missing
the override header logic.

* Rename methods

* Don't create Etcd user

* Use ntk148v/jwt-middleware

jwt-auth returns an access_token, refresh_token
& csrf string. It seems be too much in our case,
just keep it simple as much as possible.

jwt-middleware allows to set the custom content
in request context. It would be useful if there
is other middlewares suppose to use this data.

* Create admin user

* Update getting-started

* [WIP] Add RBAC proposal

* Update Config struct

- Add JWT Configuration to prepare rework
authentication & authorization.
- Remove GlobalConfig.

* Init a new Auth module

* Nit:Remove return

* Update getting-started

* WIP Init user logic

* Add bcrypt functions

* Init a new Authentication mechanism

Using jwt-auth lib [1], there are two authentication
options now:
- Cookie.
- Bearer tokens.

[1] https://github.com/adam-hanna/jwt-auth

* Update rbac proposal

* Update go.mod

* Use ntk148v/jwt-auth module

* Revert disable CSRF

* Move signout to restricted route

* Grant role with valid permissions when creating user

* Fix wrong HTTP status code in error response

HTTP status code always be 200 OK even if
there is an error response due to missing
the override header logic.

* Rename methods

* Don't create Etcd user

* Use ntk148v/jwt-middleware

jwt-auth returns an access_token, refresh_token
& csrf string. It seems be too much in our case,
just keep it simple as much as possible.

jwt-middleware allows to set the custom content
in request context. It would be useful if there
is other middlewares suppose to use this data.

* Create admin user

* Update vendor

* Add Casbin policy engine for authorization

Use an authorization library Casbin [1]

[1] https://github.com/casbin/casbin

* Create users API is restricted

* If this is an existing user, 400 is more suitable than 401

* Create users and token submodules

* Trim the key(s)

- Without trim, the key path may include double sequential
slash '/', for example: 'test//users'.
- The etcd namespace should end with slash '/'. Here is the
workaround, without it, the key path will be `<namespace><key>`.

* Include only the username

* Add Authorizer middleware

Authorizer is a middleware checks whether the user is
allowed to perform the request. It uses Casbin Enforcer
as the Policy Engine.

* Add LICENSE header

* Update method name

* Force reload the policies before enforcing

* Add RemoveUser handler

* Add policy handlers

- AddPolicy & RemovePolicy
- TODO - ListPolicies

* Update routing

* Remove etcd-adapter v1.1.0

* Update proposal

* Add listUsers API

* Package-scope, not public

* Use request's body to add/remove policies

Using body instead of form is more flexible, allow
user to add/remove more than one policy at once.

* Update docs

* Update vendor

* Remove unknown characters

* Update docs

* Allow user to configure bcrypt cost

* Remove redundant middlewares

* Return status code 401

Follow: https://i.stack.imgur.com/ppsbq.jpg

* Update api/token.go

Co-authored-by: vtdat <tuandatk25a@gmail.com>

* Raise JWT TTL to 60m

* Allow everyone to view clouds

* Remove user in etcd

* Remove redundant block

* Revert trim path

Co-authored-by: vtdat <tuandatk25a@gmail.com>
  • Loading branch information
ntk148v and vtdat authored Jun 22, 2020
1 parent eb806d1 commit ec668f6
Show file tree
Hide file tree
Showing 126 changed files with 12,681 additions and 310 deletions.
128 changes: 89 additions & 39 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,85 +20,136 @@ import (
"net/http/pprof"
"time"

"github.com/casbin/casbin/v2"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/gorilla/mux"
"github.com/jinzhu/copier"
etcdadapter "github.com/ntk148v/etcd-adapter"
"github.com/ntk148v/jwt-middleware"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
etcdv3 "go.etcd.io/etcd/clientv3"

"github.com/vCloud-DFTBA/faythe/config"
"github.com/vCloud-DFTBA/faythe/middleware"
"github.com/vCloud-DFTBA/faythe/pkg/cluster"
"github.com/vCloud-DFTBA/faythe/pkg/common"
"github.com/vCloud-DFTBA/faythe/pkg/model"
)

// API provides registration of handlers for API routes
type API struct {
logger log.Logger
uptime time.Time
etcdcli *common.Etcd
logger log.Logger
uptime time.Time
etcdcli *common.Etcd
jwtToken *jwt.Token
policyEngine *casbin.Enforcer
}

// New returns a new API.
func New(l log.Logger, e *common.Etcd) *API {
func New(l log.Logger, e *common.Etcd) (*API, error) {
jwtCfg := config.Get().JWTConfig
token, err := jwt.NewToken(jwt.Options{
SigningMethod: jwtCfg.SigningMethod,
PrivateKeyLocation: jwtCfg.PrivateKeyLocation,
PublicKeyLocation: jwtCfg.PublicKeyLocation,
IsBearerToken: jwtCfg.IsBearerToken,
UserProperty: jwtCfg.UserProperty,
TTL: jwtCfg.TTL,
}, nil)

if err != nil {
// Exit immediately
return nil, errors.Wrapf(err, "Error initializing the JWT instance")
}
if l == nil {
l = log.NewNopLogger()
}

return &API{
logger: l,
uptime: time.Now(),
etcdcli: e,
// Init Policy engine
var etcdCfg etcdv3.Config
copier.Copy(&etcdCfg, config.Get().EtcdConfig)
adapter := etcdadapter.NewAdapter(etcdCfg, cluster.ClusterID, model.DefaultPoliciesPrefix)
// NOTE(kiennt): Hardcode here, don't allow users to configure
// the Casbin model themselves. Cannot handle at this time.
policyEngine, err := casbin.NewEnforcer("api/author_model.conf", adapter)

a := &API{
logger: l,
uptime: time.Now(),
etcdcli: e,
jwtToken: token,
policyEngine: policyEngine,
}
// Create an admin user & grant permissions
if err = a.createAdminUser(); err != nil {
return nil, errors.Wrap(err, "Error creating admin user")
}
return a, nil
}

// RegisterPublicRouter registers the Authentication API which has no
// Authentication middleware
func (a *API) RegisterPublicRouter(r *mux.Router) {
r.HandleFunc("/login", a.getToken).Methods("OPTIONS", "GET")

// Register registers the API handlers under their correct routes
// in the given router.
func (a *API) Register(r *mux.Router) {
mw := middleware.New(log.With(a.logger, "component", "transport middleware"))
// General middleware
r.Use(mw.Instrument, mw.Logging, mw.RestrictDomain, mw.HandleCors)
// Unrestricted subrouter
pubr := r.PathPrefix("/public").Subrouter()
pubr.HandleFunc("/tokens", a.issueToken).Methods("OPTIONS", "POST")
// Prometheus golang metrics
r.Handle("/metrics", promhttp.Handler()).Methods("GET")
pubr.Handle("/metrics", promhttp.Handler()).Methods("GET")

r.HandleFunc("/", a.index).Methods("GET")
r.HandleFunc("/status", a.status).Methods("GET")
pubr.HandleFunc("/", a.index).Methods("GET")
pubr.HandleFunc("/status", a.status).Methods("GET")

// Profiling endpoints
cfg := config.Get().GlobalConfig
cfg := config.Get()
if cfg.EnableProfiling {
r.HandleFunc("/debug/pprof/", pprof.Index)
r.Handle("/debug/pprof/{profile}", http.DefaultServeMux)
pubr.HandleFunc("/debug/pprof/", pprof.Index)
pubr.Handle("/debug/pprof/{profile}", http.DefaultServeMux)
}
}

// Register registers the API handlers under their correct routes
// in the given router.
func (a *API) Register(r *mux.Router) {
// Restricted subrouter
resr := r.PathPrefix("/").Subrouter()
resr.Use(jwt.Authenticator(a.jwtToken), middleware.Authorizer(a.policyEngine))

// User endpoints
resr.HandleFunc("/users", a.addUser).Methods("OPTIONS", "POST")
resr.HandleFunc("/users/{user:[a-z 0-9]+}", a.removeUser).Methods("OPTIONS", "DELETE")
resr.HandleFunc("/users", a.listUsers).Methods("OPTIONS", "GET")

// Policy endpoints
resr.HandleFunc("/policies/{user:[a-z 0-9]+}", a.addPolicies).Methods("OPTIONS", "POST")
resr.HandleFunc("/policies/{user:[a-z 0-9]+}", a.removePolicies).Methods("OPTIONS", "DELETE")
// Cloud endpoints
r.HandleFunc("/clouds/{provider}", a.registerCloud).Methods("OPTIONS", "POST")
r.HandleFunc("/clouds", a.listClouds).Methods("OPTIONS", "GET")
r.HandleFunc("/clouds/{id:[a-z 0-9]+}", a.unregisterCloud).Methods("OPTIONS", "DELETE")
r.HandleFunc("/clouds/{id:[a-z 0-9]+}", a.updateCloud).Methods("OPTIONS", "PUT")
resr.HandleFunc("/clouds/{provider}", a.registerCloud).Methods("OPTIONS", "POST")
resr.HandleFunc("/clouds", a.listClouds).Methods("OPTIONS", "GET")
resr.HandleFunc("/clouds/{id:[a-z 0-9]+}", a.unregisterCloud).Methods("OPTIONS", "DELETE")
resr.HandleFunc("/clouds/{id:[a-z 0-9]+}", a.updateCloud).Methods("OPTIONS", "PUT")

// Scaler endpoints
r.HandleFunc("/scalers/{provider_id:[a-z 0-9]+}", a.createScaler).Methods("OPTIONS", "POST")
r.HandleFunc("/scalers/{provider_id:[a-z 0-9]+}", a.listScalers).Methods("OPTIONS", "GET")
r.HandleFunc("/scalers/{provider_id:[a-z 0-9]+}/{id:[a-z 0-9]+}",
resr.HandleFunc("/scalers/{provider_id:[a-z 0-9]+}", a.createScaler).Methods("OPTIONS", "POST")
resr.HandleFunc("/scalers/{provider_id:[a-z 0-9]+}", a.listScalers).Methods("OPTIONS", "GET")
resr.HandleFunc("/scalers/{provider_id:[a-z 0-9]+}/{id:[a-z 0-9]+}",
a.deleteScaler).Methods("OPTIONS", "DELETE")
r.HandleFunc("/scalers/{provider_id:[a-z 0-9]+}/{id:[a-z 0-9]+}",
resr.HandleFunc("/scalers/{provider_id:[a-z 0-9]+}/{id:[a-z 0-9]+}",
a.updateScaler).Methods("OPTIONS", "PUT")

// Name Resolver endpoints
r.HandleFunc("/nresolvers", a.listNResolvers).Methods("OPTIONS", "GET")
resr.HandleFunc("/nresolvers", a.listNResolvers).Methods("OPTIONS", "GET")

// Healer endpoints
r.HandleFunc("/healers/{provider_id:[a-z 0-9]+}", a.createHealer).Methods("OPTIONS", "POST")
r.HandleFunc("/healers/{provider_id:[a-z 0-9]+}", a.listHealers).Methods("OPTIONS", "GET")
r.HandleFunc("/healers/{provider_id:[a-z 0-9]+}/{id:[a-z 0-9]+}",
resr.HandleFunc("/healers/{provider_id:[a-z 0-9]+}", a.createHealer).Methods("OPTIONS", "POST")
resr.HandleFunc("/healers/{provider_id:[a-z 0-9]+}", a.listHealers).Methods("OPTIONS", "GET")
resr.HandleFunc("/healers/{provider_id:[a-z 0-9]+}/{id:[a-z 0-9]+}",
a.deleteHealer).Methods("OPTIONS", "DELETE")

// Silences endpoints
r.HandleFunc("/silences/{provider_id:[a-z 0-9]+}", a.createSilence).Methods("OPTIONS", "POST")
r.HandleFunc("/silences/{provider_id:[a-z 0-9]+}", a.listSilences).Methods("OPTIONS", "GET")
r.HandleFunc("/silences/{provider_id:[a-z 0-9]+}/{id:[a-z 0-9]+}", a.expireSilence).Methods("OPTIONS", "DELETE")
resr.HandleFunc("/silences/{provider_id:[a-z 0-9]+}", a.createSilence).Methods("OPTIONS", "POST")
resr.HandleFunc("/silences/{provider_id:[a-z 0-9]+}", a.listSilences).Methods("OPTIONS", "GET")
resr.HandleFunc("/silences/{provider_id:[a-z 0-9]+}/{id:[a-z 0-9]+}", a.expireSilence).Methods("OPTIONS", "DELETE")
}

func (a *API) receive(req *http.Request, v interface{}) error {
Expand All @@ -124,7 +175,6 @@ func (a *API) respondError(w http.ResponseWriter, e apiError) {
if err != nil {
level.Error(a.logger).Log("msg", "Error marshalling JSON", "err", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} else {
if _, err := w.Write(b); err != nil {
level.Error(a.logger).Log("msg", "Failed to write data to connection", "err", err)
Expand Down
65 changes: 0 additions & 65 deletions api/auth.go

This file was deleted.

11 changes: 11 additions & 0 deletions api/author_model.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)
135 changes: 135 additions & 0 deletions api/policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) 2020 Kien Nguyen-Tuan <kiennt2609@gmail.com>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package api

import (
"crypto"
"net/http"

"github.com/gorilla/mux"
"github.com/pkg/errors"

"github.com/vCloud-DFTBA/faythe/pkg/common"
"github.com/vCloud-DFTBA/faythe/pkg/model"
)

// addPolicies allows to add more than one policys at once.
func (a *API) addPolicies(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
user := vars["user"]
// Check an user is existing.
path := common.Path(model.DefaultUsersPrefix, common.Hash(user, crypto.MD5))
resp, err := a.etcdcli.DoGet(path)
if err != nil {
a.respondError(w, apiError{
code: http.StatusInternalServerError,
err: err,
})
return
}
if len(resp.Kvs) == 0 {
a.respondError(w, apiError{
code: http.StatusBadRequest,
err: errors.New("Unknown user"),
})
return
}

var (
pols model.Polices
rules [][]string
)
if err := a.receive(req, &pols); err != nil {
a.respondError(w, apiError{
code: http.StatusBadRequest,
err: err,
})
return
}
for _, p := range pols {
rules = append(rules, []string{user, p.Path, p.Method})
}
// Add new policy to Etcd
if _, err := a.policyEngine.AddPolicies(rules); err != nil {
a.respondError(w, apiError{
code: http.StatusInternalServerError,
err: err,
})
return
}
if err := a.policyEngine.SavePolicy(); err != nil {
a.respondError(w, apiError{
code: http.StatusInternalServerError,
err: err,
})
return
}
a.respondSuccess(w, http.StatusOK, nil)
return
}

// removePolicies allows to remove more than one policys at once.
func (a *API) removePolicies(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
user := vars["user"]
// Check an user is existing.
path := common.Path(model.DefaultUsersPrefix, common.Hash(user, crypto.MD5))
resp, err := a.etcdcli.DoGet(path)
if err != nil {
a.respondError(w, apiError{
code: http.StatusInternalServerError,
err: err,
})
return
}
if len(resp.Kvs) == 0 {
a.respondError(w, apiError{
code: http.StatusBadRequest,
err: errors.New("Unknown user"),
})
return
}
var (
pols model.Polices
rules [][]string
)
if err := a.receive(req, &pols); err != nil {
a.respondError(w, apiError{
code: http.StatusBadRequest,
err: err,
})
return
}
for _, p := range pols {
rules = append(rules, []string{user, p.Path, p.Method})
}
// Remove policy from Etcd
if _, err := a.policyEngine.RemovePolicies(rules); err != nil {
a.respondError(w, apiError{
code: http.StatusInternalServerError,
err: err,
})
return
}
if err := a.policyEngine.SavePolicy(); err != nil {
a.respondError(w, apiError{
code: http.StatusInternalServerError,
err: err,
})
return
}
a.respondSuccess(w, http.StatusOK, nil)
return
}
Loading

0 comments on commit ec668f6

Please sign in to comment.