Skip to content

Commit

Permalink
Merge branch 'feature/policies'
Browse files Browse the repository at this point in the history
  • Loading branch information
lonelycode committed Feb 25, 2015
2 parents ce4a399 + b5af83f commit 3241f26
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 1 deletion.
62 changes: 62 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,67 @@
# DEV
- Added LDAP StorageHandler, enables basic key lookups from an LDAP service
- Added Policies feature, you can now define key policies for keys you generate:
- Create a policies/policies.json file
- Set the appropriate arguments in tyk.conf file:

"policies": {
"policy_source": "file",
"policy_record_name": "./policies/policies.json"
}

- Create a policy, they look like this:

{
"default": {
"rate": 1000,
"per": 1,
"quota_max": 100,
"quota_renewal_rate": 60,
"access_rights": {
"41433797848f41a558c1573d3e55a410": {
"api_name": "My API",
"api_id": "41433797848f41a558c1573d3e55a410",
"versions": [
"Default"
]
}
},
"org_id": "54de205930c55e15bd000001",
"hmac_enabled": false
}
}

- Add a `apply_policy_id` field to your Session object when you create a key with your policy ID (in this case the ID is `default`)
- Reload Tyk
- Policies will be applied to Keys when they are loaded form Redis, and the updated i nRedis so they can be ueried if necessary

- Added granular path white-list: It is now possible to define at the key level what access permissions a key has, this is a white-list of regex keys and apply to a whole API definition. Granular permissions are applied *after* version-based (global) ones in the api-definition. These granular permissions take the form a new field in the access rights field in either a policy definition or a session object in the new `allowed_urls` field:

{
"default": {
"rate": 1000,
"per": 1,
"quota_max": 100,
"quota_renewal_rate": 60,
"access_rights": {
"41433797848f41a558c1573d3e55a410": {
"api_name": "My API",
"api_id": "41433797848f41a558c1573d3e55a410",
"versions": [
"Default"
],
"allowed_urls": {
"/resource/(.*)": {
"methods": ["GET", "POST"]
}
}
}
},
"org_id": "54de205930c55e15bd000001",
"hmac_enabled": false
}
}

# v1.5
- Added caching middleware
Expand Down
4 changes: 4 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ type Config struct {
TemplatePath string `json:"template_path"`
TykJSPath string `json:"tyk_js_path"`
MiddlewarePath string `json:"middleware_path"`
Policies struct {
PolicySource string `json:"policy_source"`
PolicyRecordName string `json:"policy_record_name"`
} `json:"policies"`
UseDBAppConfigs bool `json:"use_db_app_configs"`
AppPath string `json:"app_path"`
Storage struct {
Expand Down
29 changes: 28 additions & 1 deletion handler_success.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@ func (t TykMiddleware) GetOrgSession(key string) (SessionState, bool) {
return thisSession, found
}

// ApplyPolicyIfExists will check if a policy is loaded, if it is, it will overwrite the session state to use the policy values
func (t TykMiddleware) ApplyPolicyIfExists(key string, thisSession *SessionState) {
if thisSession.ApplyPolicyID != "" {
log.Debug("Session has policy, checking")
policy, ok := Policies[thisSession.ApplyPolicyID]
if ok {
log.Debug("Found policy, applying")
thisSession.Allowance = policy.Rate // This is a legacy thing, merely to make sure output is consistent. Needs to be purged
thisSession.Rate = policy.Rate
thisSession.Per = policy.Per
thisSession.QuotaMax = policy.QuotaMax
thisSession.QuotaRenewalRate = policy.QuotaRenewalRate
thisSession.AccessRights = policy.AccessRights
thisSession.HMACEnabled = policy.HMACEnabled

// Update the session in the session manager in case it gets called again
t.Spec.SessionManager.UpdateSession(key, *thisSession, t.Spec.APIDefinition.SessionLifetime)
log.Debug("Policy applied to key")
}
}
}

// CheckSessionAndIdentityForValidKey will check first the Session store for a valid key, if not found, it will try
// the Auth Handler, if not found it will fail
func (t TykMiddleware) CheckSessionAndIdentityForValidKey(key string) (SessionState, bool) {
Expand All @@ -50,6 +72,9 @@ func (t TykMiddleware) CheckSessionAndIdentityForValidKey(key string) (SessionSt
thisSession, found = t.Spec.SessionManager.GetSessionDetail(key)
if found {
// If exists, assume it has been authorized and pass on

// Check for a policy, if there is a policy, pull it and overwrite the session values
t.ApplyPolicyIfExists(key, &thisSession)
return thisSession, true
}

Expand All @@ -59,9 +84,11 @@ func (t TykMiddleware) CheckSessionAndIdentityForValidKey(key string) (SessionSt
if found {
// If not in Session, and got it from AuthHandler, create a session with a new TTL
log.Info("Recreating session for key: ", key)
// Check for a policy, if there is a policy, pull it and overwrite the session values
t.ApplyPolicyIfExists(key, &thisSession)
t.Spec.SessionManager.UpdateSession(key, thisSession, t.Spec.APIDefinition.SessionLifetime)
}

return thisSession, found
}

Expand Down
22 changes: 22 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var analytics = RedisAnalyticsHandler{}
var profileFile = &os.File{}
var GlobalEventsJSVM = &JSVM{}
var doMemoryProfile bool
var Policies = make(map[string]Policy)

//var genericOsinStorage *RedisOsinStorageInterface
var ApiSpecRegister = make(map[string]*APISpec)
Expand Down Expand Up @@ -103,6 +104,21 @@ func getAPISpecs() []APISpec {
return APISpecs
}

func getPolicies() {
log.Info("Loading policies")
if config.Policies.PolicyRecordName == "" {
log.Info("No policy record name defined, skipping...")
return
}

if config.Policies.PolicySource == "mongo" {
log.Info("Using Policies from Mongo DB")
Policies = LoadPoliciesFromMongo(config.Policies.PolicyRecordName)
} else {
Policies = LoadPoliciesFromFile(config.Policies.PolicyRecordName)
}
}

// Set up default Tyk control API endpoints - these are global, so need to be added first
func loadAPIEndpoints(Muxer *http.ServeMux) {
// set up main API handlers
Expand Down Expand Up @@ -345,6 +361,7 @@ func loadApps(APISpecs []APISpec, Muxer *http.ServeMux) {
CreateMiddleware(&KeyExpired{tykMiddleware}, tykMiddleware),
CreateMiddleware(&AccessRightsCheck{tykMiddleware}, tykMiddleware),
CreateMiddleware(&RateLimitAndQuotaCheck{tykMiddleware}, tykMiddleware),
CreateMiddleware(&GranularAccessMiddleware{tykMiddleware}, tykMiddleware),
CreateMiddleware(&TransformMiddleware{tykMiddleware}, tykMiddleware),
CreateMiddleware(&TransformHeaders{TykMiddleware: tykMiddleware}, tykMiddleware),
CreateMiddleware(&RedisCacheMiddleware{TykMiddleware: tykMiddleware, CacheStore: CacheStore}, tykMiddleware),
Expand Down Expand Up @@ -398,6 +415,9 @@ func ReloadURLStructure() {
loadAPIEndpoints(newMuxes)
specs := getAPISpecs()
loadApps(specs, newMuxes)

// Load the API Policies
getPolicies()

http.DefaultServeMux = newMuxes
log.Info("Reload complete")
Expand Down Expand Up @@ -512,6 +532,7 @@ func main() {
// Accept connections in a new goroutine.
specs := getAPISpecs()
loadApps(specs, http.DefaultServeMux)
getPolicies()
go http.Serve(l, nil)

} else {
Expand All @@ -520,6 +541,7 @@ func main() {
log.Println("Resuming listening on", l.Addr())
specs := getAPISpecs()
loadApps(specs, http.DefaultServeMux)
getPolicies()
go http.Serve(l, nil)

// Kill the parent, now that the child has started successfully.
Expand Down
74 changes: 74 additions & 0 deletions middleware_granular_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"errors"
"github.com/gorilla/context"
"net/http"
"regexp"
"github.com/Sirupsen/logrus"
)

// GranularAccessMiddleware will check if a URL is specifically enabled for the key
type GranularAccessMiddleware struct {
TykMiddleware
}

type GranularAccessMiddlewareConfig struct {}

func (m *GranularAccessMiddleware) New() {}

// GetConfig retrieves the configuration from the API config - we user mapstructure for this for simplicity
func (m *GranularAccessMiddleware) GetConfig() (interface{}, error) {
return nil, nil
}

// ProcessRequest will run any checks on the request on the way through the system, return an error to have the chain fail
func (m *GranularAccessMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, configuration interface{}) (error, int) {
thisSessionState := context.Get(r, SessionData).(SessionState)
authHeaderValue := context.Get(r, AuthHeaderValue).(string)

sessionVersionData, foundAPI := thisSessionState.AccessRights[m.Spec.APIID]

if foundAPI == false {
log.Debug("Version not found")
return nil, 200
}

if sessionVersionData.AllowedURLs == nil {
log.Debug("No allowed URLS")
return nil, 200
}

for url_regex, accessSpec := range(sessionVersionData.AllowedURLs) {
log.Debug("Checking: ", r.URL.Path)
log.Debug("Against: ", url_regex)
asRegex, regexpErr := regexp.Compile(url_regex)

if regexpErr != nil {
log.Error("Regex error: ", regexpErr)
return nil, 200
}

match := asRegex.MatchString(r.URL.Path)
if match {
log.Debug("Match!")
for _, method := range(accessSpec.Methods) {
if method == r.Method {
return nil, 200
}
}
}
}

// No paths matched, disallow
log.WithFields(logrus.Fields{
"path": r.URL.Path,
"origin": r.RemoteAddr,
"key": authHeaderValue,
"api_found": false,
}).Info("Attempted access to unauthorised endpoint (Granular).")

return errors.New("Access to this resource has been disallowed"), 403


}
61 changes: 61 additions & 0 deletions policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"io/ioutil"
"encoding/json"
"labix.org/v2/mgo"
"labix.org/v2/mgo/bson"
)

type Policy struct {
ID string `bson:"_id, ommitempty" json:"id"`
Rate float64 `bson:"rate" json:"rate"`
Per float64 `bson:"per" json:"per"`
QuotaMax int64 `bson:"quota_max" json:"quota_max"`
QuotaRenewalRate int64 `bson:"quota_renewal_rate" json:"quota_renewal_rate"`
AccessRights map[string]AccessDefinition `bson:"access_rights" json:"access_rights"`
HMACEnabled bool `bson:"hmac_enabled" json:"hmac_enabled"`
Active bool `bson:"active" json:"active"`
}

func LoadPoliciesFromFile(filePath string) map[string]Policy {
policies := make(map[string]Policy)

policyConfig, err := ioutil.ReadFile(filePath)
if err != nil {
log.Error("Couldn't load policy file: ", err)
return policies
}

mErr := json.Unmarshal(policyConfig, &policies)
if mErr != nil {
log.Error("Couldn't unmarshal policies: ", mErr)
}

return policies
}

// LoadPoliciesFromMongo will connect and download POlicies from a Mongo DB instance.
func LoadPoliciesFromMongo(collectionName string) map[string]Policy {
policies := make(map[string]Policy)

dbSession, dErr := mgo.Dial(config.AnalyticsConfig.MongoURL)
if dErr != nil {
log.Error("Mongo connection failed:", dErr)
}

policyCollection := dbSession.DB("").C(collectionName)

search := bson.M{
"active": true,
}

mongoErr := policyCollection.Find(search).All(&policies)

if mongoErr != nil {
log.Error("Could not find any policy configs!")
return policies
}

return policies
}
7 changes: 7 additions & 0 deletions session_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import (
"time"
)

// AccessSpecs define what URLS a user has access to an what methods are enabled
type AccessSpec struct {
Methods []string `json:"methods"`
}

// AccessDefinition defines which versions of an API a key has access to
type AccessDefinition struct {
APIiName string `json:"api_name"`
APIID string `json:"api_id"`
Versions []string `json:"versions"`
AllowedURLs map[string]AccessSpec `json:"allowed_urls"` // mapped string MUST be a valid regex
}

// SessionState objects represent a current API session, mainly used for rate limiting.
Expand All @@ -31,6 +37,7 @@ type SessionState struct {
HMACEnabled bool `json:"hmac_enabled"`
HmacSecret string `json:"hmac_string"`
IsInactive bool `json:"is_inactive"`
ApplyPolicyID string `json:"apply_policy_id"`
MetaData interface{} `json:"meta_data"`
}

Expand Down
Binary file modified stage/tyk
Binary file not shown.

0 comments on commit 3241f26

Please sign in to comment.