2020-02-03 11:16:02 +03:00
package service
import (
"context"
2020-03-24 16:48:37 +03:00
"fmt"
2020-04-15 13:31:19 +03:00
"sort"
2020-03-23 19:19:30 +03:00
"strings"
2020-02-03 11:16:02 +03:00
"time"
"github.com/micro/go-micro/v2/auth"
2020-03-30 18:23:00 +03:00
pb "github.com/micro/go-micro/v2/auth/service/proto"
2020-03-23 19:19:30 +03:00
"github.com/micro/go-micro/v2/auth/token"
"github.com/micro/go-micro/v2/auth/token/jwt"
2020-02-03 11:26:57 +03:00
"github.com/micro/go-micro/v2/client"
2020-03-24 16:48:37 +03:00
log "github.com/micro/go-micro/v2/logger"
2020-03-25 21:34:13 +03:00
"github.com/micro/go-micro/v2/util/jitter"
2020-02-03 11:16:02 +03:00
)
// svc is the service implementation of the Auth interface
type svc struct {
options auth . Options
2020-03-30 18:23:00 +03:00
auth pb . AuthService
rule pb . RulesService
2020-03-23 19:19:30 +03:00
jwt token . Provider
2020-02-03 11:16:02 +03:00
}
func ( s * svc ) String ( ) string {
return "service"
}
2020-03-23 19:19:30 +03:00
func ( s * svc ) Init ( opts ... auth . Option ) {
2020-02-03 11:16:02 +03:00
for _ , o := range opts {
o ( & s . options )
}
2020-05-11 19:57:39 +03:00
if s . options . Client == nil {
s . options . Client = client . DefaultClient
}
2020-05-13 19:35:57 +03:00
2020-05-11 19:57:39 +03:00
s . auth = pb . NewAuthService ( "go.micro.auth" , s . options . Client )
s . rule = pb . NewRulesService ( "go.micro.auth" , s . options . Client )
2020-02-03 11:16:02 +03:00
2020-03-23 19:19:30 +03:00
// if we have a JWT public key passed as an option,
// we can decode tokens with the type "JWT" locally
// and not have to make an RPC call
if key := s . options . PublicKey ; len ( key ) > 0 {
s . jwt = jwt . NewTokenProvider ( token . WithPublicKey ( key ) )
}
2020-02-03 11:16:02 +03:00
}
2020-02-10 11:26:28 +03:00
func ( s * svc ) Options ( ) auth . Options {
return s . options
}
2020-03-23 19:19:30 +03:00
// Generate a new account
2020-04-01 19:20:02 +03:00
func ( s * svc ) Generate ( id string , opts ... auth . GenerateOption ) ( * auth . Account , error ) {
2020-02-03 11:16:02 +03:00
options := auth . NewGenerateOptions ( opts ... )
2020-03-30 18:23:00 +03:00
rsp , err := s . auth . Generate ( context . TODO ( ) , & pb . GenerateRequest {
2020-03-31 12:06:13 +03:00
Id : id ,
2020-03-31 21:01:43 +03:00
Type : options . Type ,
2020-04-01 19:20:02 +03:00
Secret : options . Secret ,
2020-03-31 12:06:13 +03:00
Roles : options . Roles ,
Metadata : options . Metadata ,
2020-03-31 21:01:43 +03:00
Provider : options . Provider ,
2020-03-31 12:06:13 +03:00
Namespace : options . Namespace ,
2020-05-14 13:06:22 +03:00
} )
2020-02-03 11:16:02 +03:00
if err != nil {
return nil , err
}
2020-03-23 19:19:30 +03:00
return serializeAccount ( rsp . Account ) , nil
2020-02-03 11:16:02 +03:00
}
2020-03-23 19:19:30 +03:00
// Grant access to a resource
func ( s * svc ) Grant ( role string , res * auth . Resource ) error {
2020-03-30 18:23:00 +03:00
_ , err := s . rule . Create ( context . TODO ( ) , & pb . CreateRequest {
2020-03-26 16:12:43 +03:00
Role : role ,
2020-03-30 18:23:00 +03:00
Access : pb . Access_GRANTED ,
Resource : & pb . Resource {
2020-04-02 19:01:06 +03:00
Namespace : res . Namespace ,
Type : res . Type ,
Name : res . Name ,
Endpoint : res . Endpoint ,
2020-03-23 19:19:30 +03:00
} ,
2020-05-14 13:06:22 +03:00
} )
2020-03-23 19:19:30 +03:00
return err
}
2020-02-03 11:16:02 +03:00
2020-03-23 19:19:30 +03:00
// Revoke access to a resource
func ( s * svc ) Revoke ( role string , res * auth . Resource ) error {
2020-03-30 18:23:00 +03:00
_ , err := s . rule . Delete ( context . TODO ( ) , & pb . DeleteRequest {
2020-03-26 16:12:43 +03:00
Role : role ,
2020-03-30 18:23:00 +03:00
Access : pb . Access_GRANTED ,
Resource : & pb . Resource {
2020-04-02 19:01:06 +03:00
Namespace : res . Namespace ,
Type : res . Type ,
Name : res . Name ,
Endpoint : res . Endpoint ,
2020-03-23 19:19:30 +03:00
} ,
2020-05-14 13:06:22 +03:00
} )
2020-02-03 11:16:02 +03:00
return err
}
2020-03-23 19:19:30 +03:00
// Verify an account has access to a resource
func ( s * svc ) Verify ( acc * auth . Account , res * auth . Resource ) error {
2020-04-07 18:24:51 +03:00
// set the namespace on the resource
if len ( res . Namespace ) == 0 {
res . Namespace = s . Options ( ) . Namespace
}
2020-03-24 16:48:37 +03:00
queries := [ ] [ ] string {
2020-04-02 19:01:06 +03:00
{ res . Namespace , res . Type , res . Name , res . Endpoint } , // check for specific role, e.g. service.foo.ListFoo:admin (role is checked in accessForRule)
{ res . Namespace , res . Type , res . Name , "*" } , // check for wildcard endpoint, e.g. service.foo*
{ res . Namespace , res . Type , "*" } , // check for wildcard name, e.g. service.*
{ res . Namespace , "*" } , // check for wildcard type, e.g. *
{ "*" } , // check for wildcard namespace
2020-03-24 16:48:37 +03:00
}
// endpoint is a url which can have wildcard excludes, e.g.
// "/foo/*" will allow "/foo/bar"
if comps := strings . Split ( res . Endpoint , "/" ) ; len ( comps ) > 1 {
for i := 1 ; i < len ( comps ) ; i ++ {
wildcard := fmt . Sprintf ( "%v/*" , strings . Join ( comps [ 0 : i ] , "/" ) )
queries = append ( queries , [ ] string { res . Type , res . Name , wildcard } )
}
}
2020-04-14 14:32:59 +03:00
// set a default account id / namespace to log
2020-04-02 19:01:06 +03:00
logID := acc . ID
if len ( logID ) == 0 {
logID = "[no account]"
}
2020-04-14 14:32:59 +03:00
logNamespace := acc . Namespace
if len ( logNamespace ) == 0 {
logNamespace = "[no namespace]"
}
2020-04-02 19:01:06 +03:00
2020-03-24 16:48:37 +03:00
for _ , q := range queries {
for _ , rule := range s . listRules ( q ... ) {
2020-03-26 16:12:43 +03:00
switch accessForRule ( rule , acc , res ) {
2020-03-30 18:23:00 +03:00
case pb . Access_UNKNOWN :
2020-03-26 16:12:43 +03:00
continue // rule did not specify access, check the next rule
2020-03-30 18:23:00 +03:00
case pb . Access_GRANTED :
2020-04-14 14:32:59 +03:00
log . Tracef ( "%v:%v granted access to %v:%v:%v:%v by rule %v" , logNamespace , logID , res . Namespace , res . Type , res . Name , res . Endpoint , rule . Id )
2020-03-26 16:12:43 +03:00
return nil // rule grants the account access to the resource
2020-03-30 18:23:00 +03:00
case pb . Access_DENIED :
2020-04-14 14:32:59 +03:00
log . Tracef ( "%v:%v denied access to %v:%v:%v:%v by rule %v" , logNamespace , logID , res . Namespace , res . Type , res . Name , res . Endpoint , rule . Id )
2020-03-26 20:35:28 +03:00
return auth . ErrForbidden // rule denies access to the resource
2020-03-24 16:48:37 +03:00
}
}
}
2020-03-26 16:12:43 +03:00
// no rules were found for the resource, default to denying access
2020-04-14 14:32:59 +03:00
log . Tracef ( "%v:%v denied access to %v:%v:%v:%v by lack of rule (%v rules found for namespace)" , logNamespace , logID , res . Namespace , res . Type , res . Name , res . Endpoint , len ( s . listRules ( res . Namespace ) ) )
2020-03-24 16:48:37 +03:00
return auth . ErrForbidden
2020-03-23 19:19:30 +03:00
}
// Inspect a token
func ( s * svc ) Inspect ( token string ) ( * auth . Account , error ) {
2020-04-01 16:25:00 +03:00
// try to decode JWT locally and fall back to srv if an error occurs
2020-03-23 19:19:30 +03:00
if len ( strings . Split ( token , "." ) ) == 3 && s . jwt != nil {
2020-04-07 18:24:51 +03:00
return s . jwt . Inspect ( token )
2020-03-23 19:19:30 +03:00
}
2020-04-07 18:24:51 +03:00
// the token is not a JWT or we do not have the keys to decode it,
// fall back to the auth service
2020-05-14 13:06:22 +03:00
rsp , err := s . auth . Inspect ( context . TODO ( ) , & pb . InspectRequest { Token : token } )
2020-02-03 11:16:02 +03:00
if err != nil {
return nil , err
}
2020-03-23 19:19:30 +03:00
return serializeAccount ( rsp . Account ) , nil
2020-02-03 11:16:02 +03:00
}
2020-03-31 12:06:13 +03:00
// Token generation using an account ID and secret
2020-04-01 16:25:00 +03:00
func ( s * svc ) Token ( opts ... auth . TokenOption ) ( * auth . Token , error ) {
2020-03-31 12:06:13 +03:00
options := auth . NewTokenOptions ( opts ... )
2020-02-03 11:16:02 +03:00
2020-03-31 12:06:13 +03:00
rsp , err := s . auth . Token ( context . Background ( ) , & pb . TokenRequest {
2020-04-01 16:25:00 +03:00
Id : options . ID ,
Secret : options . Secret ,
RefreshToken : options . RefreshToken ,
TokenExpiry : int64 ( options . Expiry . Seconds ( ) ) ,
2020-05-14 13:06:22 +03:00
} )
2020-03-23 19:19:30 +03:00
if err != nil {
return nil , err
2020-02-03 11:16:02 +03:00
}
2020-03-23 19:19:30 +03:00
return serializeToken ( rsp . Token ) , nil
2020-02-03 11:16:02 +03:00
}
2020-03-24 16:48:37 +03:00
var ruleJoinKey = ":"
2020-03-26 16:12:43 +03:00
// accessForRule returns a rule status, indicating if a rule permits access to a
2020-03-24 16:48:37 +03:00
// resource for a given account
2020-03-30 18:23:00 +03:00
func accessForRule ( rule * pb . Rule , acc * auth . Account , res * auth . Resource ) pb . Access {
2020-05-15 12:24:30 +03:00
// a blank role permits access to the public
if rule . Role == "" {
return rule . Access
}
// a * role permits access to any user
if rule . Role == "*" && acc != nil {
2020-03-26 16:12:43 +03:00
return rule . Access
2020-03-24 16:48:37 +03:00
}
for _ , role := range acc . Roles {
if rule . Role == role {
2020-03-26 16:12:43 +03:00
return rule . Access
2020-03-24 16:48:37 +03:00
}
// allow user.anything if role is user.*
if strings . HasSuffix ( rule . Role , ".*" ) && strings . HasPrefix ( rule . Role , role + "." ) {
2020-03-26 16:12:43 +03:00
return rule . Access
2020-03-24 16:48:37 +03:00
}
}
2020-03-30 18:23:00 +03:00
return pb . Access_UNKNOWN
2020-03-24 16:48:37 +03:00
}
2020-04-02 19:01:06 +03:00
// listRules gets all the rules from the store which match the filters.
// filters are namespace, type, name and then endpoint.
2020-03-30 18:23:00 +03:00
func ( s * svc ) listRules ( filters ... string ) [ ] * pb . Rule {
2020-05-24 22:26:37 +03:00
// load rules using the client cache
allRules , err := s . loadRules ( )
if err != nil {
return [ ] * pb . Rule { }
}
2020-03-24 16:48:37 +03:00
2020-03-30 18:23:00 +03:00
var rules [ ] * pb . Rule
2020-05-24 22:26:37 +03:00
for _ , r := range allRules {
2020-04-02 19:01:06 +03:00
if len ( filters ) > 0 && r . Resource . Namespace != filters [ 0 ] {
continue
}
if len ( filters ) > 1 && r . Resource . Type != filters [ 1 ] {
continue
2020-03-24 16:48:37 +03:00
}
2020-04-02 19:01:06 +03:00
if len ( filters ) > 2 && r . Resource . Name != filters [ 2 ] {
continue
}
if len ( filters ) > 3 && r . Resource . Endpoint != filters [ 3 ] {
continue
}
rules = append ( rules , r )
2020-03-24 16:48:37 +03:00
}
2020-04-15 13:31:19 +03:00
// sort rules by priority
sort . Slice ( rules , func ( i , j int ) bool {
return rules [ i ] . Priority < rules [ j ] . Priority
} )
2020-03-24 16:48:37 +03:00
return rules
}
// loadRules retrieves the rules from the auth service
2020-05-24 22:26:37 +03:00
func ( s * svc ) loadRules ( ) ( [ ] * pb . Rule , error ) {
2020-05-22 18:52:24 +03:00
rsp , err := s . rule . List ( context . TODO ( ) , & pb . ListRequest { } , client . WithCache ( time . Minute ) )
2020-03-24 16:48:37 +03:00
if err != nil {
2020-05-24 22:26:37 +03:00
log . Debugf ( "Error listing rules: %v" , err )
return nil , err
2020-03-24 16:48:37 +03:00
}
2020-05-24 22:26:37 +03:00
return rsp . Rules , nil
2020-05-13 19:07:46 +03:00
}
2020-03-30 18:23:00 +03:00
func serializeToken ( t * pb . Token ) * auth . Token {
2020-03-23 19:19:30 +03:00
return & auth . Token {
2020-04-01 16:25:00 +03:00
AccessToken : t . AccessToken ,
RefreshToken : t . RefreshToken ,
Created : time . Unix ( t . Created , 0 ) ,
Expiry : time . Unix ( t . Expiry , 0 ) ,
2020-02-03 11:16:02 +03:00
}
2020-03-23 19:19:30 +03:00
}
2020-02-03 11:16:02 +03:00
2020-03-30 18:23:00 +03:00
func serializeAccount ( a * pb . Account ) * auth . Account {
2020-03-23 19:19:30 +03:00
return & auth . Account {
2020-04-01 16:25:00 +03:00
ID : a . Id ,
Roles : a . Roles ,
2020-04-01 19:20:02 +03:00
Secret : a . Secret ,
2020-04-01 16:25:00 +03:00
Metadata : a . Metadata ,
Provider : a . Provider ,
Namespace : a . Namespace ,
2020-03-23 19:19:30 +03:00
}
2020-02-03 11:16:02 +03:00
}
2020-05-13 19:35:57 +03:00
// NewAuth returns a new instance of the Auth service
func NewAuth ( opts ... auth . Option ) auth . Auth {
options := auth . NewOptions ( opts ... )
if options . Client == nil {
options . Client = client . DefaultClient
}
2020-05-13 19:58:03 +03:00
service := & svc {
2020-05-13 19:35:57 +03:00
auth : pb . NewAuthService ( "go.micro.auth" , options . Client ) ,
rule : pb . NewRulesService ( "go.micro.auth" , options . Client ) ,
options : options ,
}
2020-05-13 19:58:03 +03:00
// load rules periodically from the auth service
go func ( ) {
ruleTimer := time . NewTicker ( time . Second * 30 )
for {
2020-05-14 15:30:21 +03:00
<- ruleTimer . C
2020-05-13 19:58:03 +03:00
time . Sleep ( jitter . Do ( time . Second * 5 ) )
service . loadRules ( )
}
} ( )
return service
2020-05-13 19:35:57 +03:00
}