Skip to content

Commit

Permalink
Add #520 Restrict access to API endpoints using scopes in API keys
Browse files Browse the repository at this point in the history
  • Loading branch information
albinpa authored and georgepadayatti committed Nov 15, 2023
1 parent f1de48a commit 869d9e3
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 143 deletions.
170 changes: 85 additions & 85 deletions internal/http_path/v2/routes.go

Large diffs are not rendered by default.

40 changes: 0 additions & 40 deletions internal/middleware/authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import (
"context"
"log"
"net/http"
"strings"

"github.com/bb-consent/api/internal/apikey"
"github.com/bb-consent/api/internal/config"
"github.com/bb-consent/api/internal/error_handler"
"github.com/bb-consent/api/internal/iam"
"github.com/bb-consent/api/internal/idp"
Expand Down Expand Up @@ -159,39 +156,6 @@ func verifyTokenAndIdentifyRole(accessToken string, r *http.Request) error {
return nil
}

func decodeApiKey(headerValue string, w http.ResponseWriter) apikey.Claims {
claims, err := apikey.Decode(headerValue)

if err != nil {
m := "Invalid token, Authorization failed"
error_handler.Exit(http.StatusUnauthorized, m)
}

return claims
}

func performAPIKeyAuthentication(claims apikey.Claims, w http.ResponseWriter, r *http.Request) {
individualId := r.Header.Get(config.IndividualHeaderKey)

// Repository
individualRepo := individual.IndividualRepository{}
individualRepo.Init(claims.OrganisationId)

t := token.AccessToken{}
token.Set(r, t)
if len(strings.TrimSpace(individualId)) != 0 {
// fetch the individual
_, err := individualRepo.Get(individualId)
if err != nil {
m := "User does not exist, Authorization failed"
error_handler.Exit(http.StatusBadRequest, m)
}
token.SetUserToRequestContext(r, individualId, rbac.ROLE_USER)
} else {
token.SetUserToRequestContext(r, claims.OrganisationAdminId, rbac.ROLE_ADMIN)
}
}

// Authenticate Validates the token and sets the token to the context.
func Authenticate() Middleware {

Expand All @@ -210,10 +174,6 @@ func Authenticate() Middleware {
storeAccessTokenInRequestContext(headerValue, w, r)
verifyTokenAndIdentifyRole(headerValue, r)
}
if headerType == token.AuthorizationAPIKey {
claims := decodeApiKey(headerValue, w)
performAPIKeyAuthentication(claims, w, r)
}
// Call the next middleware/handler in chain
f(w, r)
}
Expand Down
66 changes: 53 additions & 13 deletions internal/middleware/authorise.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package middleware

import (
"errors"
"log"
"net/http"

Expand All @@ -17,25 +18,64 @@ func Authorize(e *casbin.Enforcer) Middleware {
// Define the http.HandlerFunc
return func(w http.ResponseWriter, r *http.Request) {

userRole := token.GetUserRole(r)
headerType, headerValue := getAccessTokenFromHeader(w, r)

// casbin enforce
res, err := e.Enforce(userRole, r.URL.Path, r.Method)
if err != nil {
m := "Failed to enforce casbin authentication;"
common.HandleError(w, http.StatusInternalServerError, m, err)
return
}
// verify rbac for token based access
if headerType == token.AuthorizationToken {
userRole := token.GetUserRole(r)

// casbin enforce
res, err := e.Enforce(userRole, r.URL.Path, r.Method)
if err != nil {
m := "Failed to enforce casbin authentication;"
common.HandleError(w, http.StatusInternalServerError, m, err)
return
}

if !res {
log.Printf("User does not have enough permissions")
m := "Unauthorized access;User doesn't have enough permissions;"
common.HandleError(w, http.StatusForbidden, m, nil)
return
if !res {
log.Printf("User does not have enough permissions")
m := "Unauthorized access;User doesn't have enough permissions;"
common.HandleError(w, http.StatusForbidden, m, nil)
return
}
}
// verify rbac for apikey based access
if headerType == token.AuthorizationAPIKey {
// decode claims
claims := decodeApiKey(headerValue, w)
res, err := verifyApiKeyScope(claims.Scopes, e, r)
if err != nil {
m := "Failed to enforce casbin authentication;"
common.HandleError(w, http.StatusInternalServerError, m, err)
return
}
if !res {
log.Printf("User does not have enough permissions")
m := "Unauthorized access;User doesn't have enough permissions;"
common.HandleError(w, http.StatusForbidden, m, nil)
return
}
}

// Call the next middleware/handler in chain
f(w, r)
}
}
}

// verifyApiKeyScope verify apikey scope
func verifyApiKeyScope(Scopes []string, e *casbin.Enforcer, r *http.Request) (bool, error) {
var res bool
var err error
for _, scope := range Scopes {
res, err = e.Enforce(scope, r.URL.Path, r.Method)
if err != nil {
m := "failed to enforce casbin authentication;"
return false, errors.New(m)
}
if res {
return true, nil
}
}
return false, nil
}
116 changes: 116 additions & 0 deletions internal/middleware/scope_based_api_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package middleware

import (
"net/http"
"strings"

"github.com/bb-consent/api/internal/apikey"
"github.com/bb-consent/api/internal/config"
"github.com/bb-consent/api/internal/error_handler"
"github.com/bb-consent/api/internal/individual"
"github.com/bb-consent/api/internal/rbac"
"github.com/bb-consent/api/internal/token"
"github.com/bb-consent/api/internal/user"
)

func decodeApiKey(headerValue string, w http.ResponseWriter) apikey.Claims {
claims, err := apikey.Decode(headerValue)

if err != nil {
m := "Invalid token, Authorization failed"
error_handler.Exit(http.StatusUnauthorized, m)
}

return claims
}

func performAPIKeyAuthentication(claims apikey.Claims, tag string, w http.ResponseWriter, r *http.Request) {

t := token.AccessToken{}

// Check if individualId is present in request header for service tag
// If present validate user
// If not present, return error
if tag == config.Service {
individualId := r.Header.Get(config.IndividualHeaderKey)

// Repository
individualRepo := individual.IndividualRepository{}
individualRepo.Init(claims.OrganisationId)

if len(strings.TrimSpace(individualId)) != 0 {
// fetch the individual
individual, err := individualRepo.Get(individualId)
if err != nil {
m := "User does not exist, Authorization failed"
error_handler.Exit(http.StatusBadRequest, m)
}
t.Email = individual.Email
t.IamID = individual.IamId
token.Set(r, t)
token.SetUserToRequestContext(r, individualId, rbac.ROLE_USER)
} else {
m := "IndividualId is not present in request header"
error_handler.Exit(http.StatusBadRequest, m)
}

} else {
// fetch organisation admin and set to context if api tag is other than service
orgAdmin, err := user.Get(claims.OrganisationAdminId)
if err != nil {
m := "User does not exist, Authorization failed"
error_handler.Exit(http.StatusBadRequest, m)
}
t.Email = orgAdmin.Email
t.IamID = orgAdmin.IamID
token.Set(r, t)
token.SetUserToRequestContext(r, claims.OrganisationAdminId, rbac.ROLE_ADMIN)
}

}

// getApiTag get api tag from route
func getApiTag(route string) string {

if strings.HasPrefix(route, "/v2/service") {
return "service"
} else if strings.HasPrefix(route, "/v2/config") {
return "config"
} else if strings.HasPrefix(route, "/v2/audit") {
return "audit"
} else if strings.HasPrefix(route, "/v2/onboard") || strings.HasPrefix(route, "/onboard") {
return "onboard"
} else {
return "unknown"
}
}

// ScopeBasedApiAccess Validates the apikey.
func ScopeBasedApiAccess() Middleware {

// Create a new Middleware
return func(f http.HandlerFunc) http.HandlerFunc {

// Define the http.HandlerFunc
return func(w http.ResponseWriter, r *http.Request) {
// To catch panic and recover the error
// Once the error is recovered respond by
// writing the error to HTTP response
defer error_handler.HandleExit(w)
headerType, headerValue := getAccessTokenFromHeader(w, r)

if headerType == token.AuthorizationAPIKey {
claims := decodeApiKey(headerValue, w)
tag := getApiTag(r.URL.Path)
if tag == "unknown" {
m := "Unknown tag in request path"
error_handler.Exit(http.StatusBadRequest, m)
}
performAPIKeyAuthentication(claims, tag, w, r)
}

// Call the next middleware/handler in chain
f(w, r)
}
}
}
67 changes: 67 additions & 0 deletions internal/rbac/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,73 @@ func GetRbacPolicies() [][]string {
{"user", "/v2/service/individual/record", "DELETE"},
{"user", "/v2/onboard/logout", "POST"},
{"organisation_admin", "/v2/onboard/logout", "POST"},
{"audit", "/v2/audit/consent-records", "GET"},
{"audit", "/v2/audit/consent-record/{consentRecordId}", "GET"},
{"audit", "/v2/audit/data-agreements", "GET"},
{"audit", "/v2/audit/data-agreement/{dataAgreementId}", "GET"},
{"audit", "/v2/audit/admin/logs", "GET"},
{"config", "/v2/config/policy", "POST"},
{"config", "/v2/config/policy/{policyId}", "(GET)|(PUT)|(DELETE)"},
{"config", "/v2/config/policy/{policyId}/revisions", "GET"},
{"config", "/v2/config/policies", "GET"},
{"config", "/v2/config/data-agreement/{dataAgreementId}", "(GET)|(PUT)|(DELETE)"},
{"config", "/v2/config/data-agreement", "POST"},
{"config", "/v2/config/data-agreements", "GET"},
{"config", "/v2/config/data-agreement/{dataAgreementId}/revisions", "GET"},
{"config", "/v2/config/data-agreement/{dataAgreementId}/revision/{revisionId}", "GET"},
{"config", "/v2/config/data-agreement/{dataAgreementId}/data-attributes", "GET"},
{"config", "/v2/config/data-agreements/data-attribute/{dataAttributeId}", "PUT"},
{"config", "/v2/config/data-agreements/data-attributes", "GET"},
{"config", "/v2/config/webhooks/event-types", "GET"},
{"config", "/v2/config/webhooks/payload/content-types", "GET"},
{"config", "/v2/config/webhooks", "GET"},
{"config", "/v2/config/webhook", "POST"},
{"config", "/v2/config/webhook/{webhookId}", "(GET)|(PUT)|(DELETE)"},
{"config", "/v2/config/webhook/{webhookId}/ping", "POST"},
{"config", "/v2/config/webhooks/{webhookId}/deliveries", "GET"},
{"config", "/v2/config/webhooks/{webhookId}/delivery/{deliveryId}", "GET"},
{"config", "/v2/config/webhooks/{webhookId}/delivery/{deliveryId}/redeliver", "POST"},
{"config", "/v2/config/idp/open-id", "POST"},
{"config", "/v2/config/idp/open-ids", "GET"},
{"config", "/v2/config/idp/open-id/{idpId}", "(GET)|(PUT)|(DELETE)"},
{"config", "/v2/config/individuals", "GET"},
{"config", "/v2/config/individual", "POST"},
{"config", "/v2/config/individual/{individualId}", "(GET)|(PUT)"},
{"config", "/v2/config/admin/apikey", "POST"},
{"config", "/v2/config/admin/apikey/{apiKeyId}", "(PUT)|(DELETE)"},
{"config", "/v2/config/admin/apikeys", "GET"},
{"service", "/v2/service/data-agreements", "GET"},
{"service", "/v2/service/data-agreement/{dataAgreementId}", "GET"},
{"service", "/v2/service/data-agreement/{dataAgreementId}/data-attributes", "GET"},
{"service", "/v2/service/policy/{policyId}", "GET"},
{"service", "/v2/service/verification/data-agreements", "GET"},
{"service", "/v2/service/verification/consent-record/{consentRecordId}", "GET"},
{"service", "/v2/service/verification/consent-records", "GET"},
{"service", "/v2/service/individual/record/consent-record/draft", "POST"},
{"service", "/v2/service/individual/record/data-agreement/{dataAgreementId}", "(GET)|(POST)"},
{"service", "/v2/service/individual/record/consent-record/{consentRecordId}", "PUT"},
{"service", "/v2/service/individual/record/consent-record", "(GET)|(POST)"},
{"service", "/v2/service/individual/record/consent-record/{consentRecordId}/signature", "(POST)|(PUT)"},
{"service", "/v2/service/individual/record/data-agreement/{dataAgreementId}/all", "GET"},
{"service", "/v2/service/individual/record/consent-record/history", "GET"},
{"service", "/v2/service/idp/open-id", "GET"},
{"service", "/v2/service/organisation", "GET"},
{"service", "/v2/service/organisation/coverimage", "GET"},
{"service", "/v2/service/organisation/logoimage", "GET"},
{"service", "/v2/service/individuals", "GET"},
{"service", "/v2/service/individual", "POST"},
{"service", "/v2/service/individual/{individualId}", "(GET)|(PUT)"},
{"service", "/v2/service/image/{imageId}", "GET"},
{"service", "/v2/service/individual/record", "DELETE"},
{"onboard", "/v2/onboard/organisation", "(GET)|(PUT)"},
{"onboard", "/v2/onboard/organisation/coverimage", "(GET)|(POST)"},
{"onboard", "/v2/onboard/organisation/logoimage", "(GET)|(POST)"},
{"onboard", "/v2/onboard/organisation", "GET"},
{"onboard", "/v2/onboard/password/reset", "PUT"},
{"onboard", "/v2/onboard/admin", "(GET)|(PUT)"},
{"onboard", "/v2/onboard/admin/avatarimage", "(GET)|(PUT)"},
{"onboard", "/v2/onboard/status", "GET"},
{"onboard", "/v2/onboard/logout", "POST"},
}

return policies
Expand Down
5 changes: 0 additions & 5 deletions internal/token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,6 @@ func SetUserToRequestContext(r *http.Request, userID string, userRole string) {
context.Set(r, userIDKey, userID)
context.Set(r, UserRoleKey, userRole)

// Set individual to request header if not present
if _, exists := r.Header[http.CanonicalHeaderKey(config.IndividualHeaderKey)]; !exists {
r.Header.Set(config.IndividualHeaderKey, userID)
}

}

// ParseTokenUnverified parses the token and returns the accessToken struct
Expand Down

0 comments on commit 869d9e3

Please sign in to comment.