2020-02-03 08:16:02 +00:00
package service
import (
"context"
2020-03-24 13:48:37 +00:00
"fmt"
2020-03-23 16:19:30 +00:00
"strings"
2020-03-24 13:48:37 +00:00
"sync"
2020-02-03 08:16:02 +00: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 16:19:30 +00:00
"github.com/micro/go-micro/v2/auth/token"
"github.com/micro/go-micro/v2/auth/token/jwt"
2020-02-03 08:26:57 +00:00
"github.com/micro/go-micro/v2/client"
2020-03-24 13:48:37 +00:00
log "github.com/micro/go-micro/v2/logger"
2020-03-25 18:34:13 +00:00
"github.com/micro/go-micro/v2/util/jitter"
2020-02-03 08:16:02 +00:00
)
// NewAuth returns a new instance of the Auth service
func NewAuth ( opts ... auth . Option ) auth . Auth {
svc := new ( svc )
svc . Init ( opts ... )
return svc
}
// 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 16:19:30 +00:00
jwt token . Provider
2020-03-24 13:48:37 +00:00
2020-03-30 18:23:00 +03:00
rules [ ] * pb . Rule
2020-03-24 13:48:37 +00:00
sync . Mutex
2020-02-03 08:16:02 +00:00
}
func ( s * svc ) String ( ) string {
return "service"
}
2020-03-23 16:19:30 +00:00
func ( s * svc ) Init ( opts ... auth . Option ) {
2020-02-03 08:16:02 +00:00
for _ , o := range opts {
o ( & s . options )
}
dc := client . DefaultClient
2020-03-30 18:23:00 +03:00
s . auth = pb . NewAuthService ( "go.micro.auth" , dc )
s . rule = pb . NewRulesService ( "go.micro.auth" , dc )
2020-02-03 08:16:02 +00:00
2020-03-23 16:19:30 +00: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-03-24 13:48:37 +00:00
// load rules periodically from the auth service
2020-03-31 12:44:34 +01:00
ruleTimer := time . NewTicker ( time . Second * 30 )
2020-03-24 13:48:37 +00:00
go func ( ) {
2020-03-25 18:34:13 +00:00
// load rules immediately on startup
s . loadRules ( )
2020-03-24 14:16:57 +00:00
for {
2020-03-31 12:44:34 +01:00
<- ruleTimer . C
2020-03-25 18:34:13 +00:00
// jitter for up to 5 seconds, this stops
// all the services calling the auth service
// at the exact same time
time . Sleep ( jitter . Do ( time . Second * 5 ) )
s . loadRules ( )
2020-03-24 14:16:57 +00:00
}
2020-03-24 13:48:37 +00:00
} ( )
2020-03-31 12:44:34 +01:00
// we have client credentials and must load a new token
// periodically
2020-04-01 14:25:00 +01:00
if len ( s . options . ID ) > 0 || len ( s . options . Secret ) > 0 {
2020-03-31 12:44:34 +01:00
tokenTimer := time . NewTicker ( time . Minute )
go func ( ) {
2020-04-01 14:25:00 +01:00
s . refreshToken ( )
2020-03-31 12:44:34 +01:00
for {
<- tokenTimer . C
// Do not get a new token if the current one has more than three
// minutes remaining. We do 3 minutes to allow multiple retires in
// the case one request fails
t := s . Options ( ) . Token
if t != nil && t . Expiry . Unix ( ) > time . Now ( ) . Add ( time . Minute * 3 ) . Unix ( ) {
continue
}
// jitter for up to 5 seconds, this stops
// all the services calling the auth service
// at the exact same time
time . Sleep ( jitter . Do ( time . Second * 5 ) )
2020-04-01 14:25:00 +01:00
s . refreshToken ( )
2020-03-31 12:44:34 +01:00
}
} ( )
}
2020-02-03 08:16:02 +00:00
}
2020-02-10 08:26:28 +00:00
func ( s * svc ) Options ( ) auth . Options {
2020-03-31 12:44:34 +01:00
s . Lock ( )
defer s . Unlock ( )
2020-02-10 08:26:28 +00:00
return s . options
}
2020-03-23 16:19:30 +00:00
// Generate a new account
2020-04-01 17:20:02 +01:00
func ( s * svc ) Generate ( id string , opts ... auth . GenerateOption ) ( * auth . Account , error ) {
2020-02-03 08:16:02 +00: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 10:06:13 +01:00
Id : id ,
2020-03-31 19:01:43 +01:00
Type : options . Type ,
2020-04-01 17:20:02 +01:00
Secret : options . Secret ,
2020-03-31 10:06:13 +01:00
Roles : options . Roles ,
Metadata : options . Metadata ,
2020-03-31 19:01:43 +01:00
Provider : options . Provider ,
2020-03-31 10:06:13 +01:00
Namespace : options . Namespace ,
2020-03-23 16:19:30 +00:00
} )
2020-02-03 08:16:02 +00:00
if err != nil {
return nil , err
}
2020-03-23 16:19:30 +00:00
return serializeAccount ( rsp . Account ) , nil
2020-02-03 08:16:02 +00:00
}
2020-03-23 16:19:30 +00: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 13:12:43 +00:00
Role : role ,
2020-03-30 18:23:00 +03:00
Access : pb . Access_GRANTED ,
Resource : & pb . Resource {
2020-04-02 17:01:06 +01:00
Namespace : res . Namespace ,
Type : res . Type ,
Name : res . Name ,
Endpoint : res . Endpoint ,
2020-03-23 16:19:30 +00:00
} ,
} )
return err
}
2020-02-03 08:16:02 +00:00
2020-03-23 16:19:30 +00: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 13:12:43 +00:00
Role : role ,
2020-03-30 18:23:00 +03:00
Access : pb . Access_GRANTED ,
Resource : & pb . Resource {
2020-04-02 17:01:06 +01:00
Namespace : res . Namespace ,
Type : res . Type ,
Name : res . Name ,
Endpoint : res . Endpoint ,
2020-03-23 16:19:30 +00:00
} ,
} )
2020-02-03 08:16:02 +00:00
return err
}
2020-03-23 16:19:30 +00:00
// Verify an account has access to a resource
func ( s * svc ) Verify ( acc * auth . Account , res * auth . Resource ) error {
2020-03-24 13:48:37 +00:00
queries := [ ] [ ] string {
2020-04-02 17:01:06 +01: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 13:48:37 +00: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-02 17:01:06 +01:00
// set a default account id to log
logID := acc . ID
if len ( logID ) == 0 {
logID = "[no account]"
}
2020-03-24 13:48:37 +00:00
for _ , q := range queries {
for _ , rule := range s . listRules ( q ... ) {
2020-03-26 13:12:43 +00:00
switch accessForRule ( rule , acc , res ) {
2020-03-30 18:23:00 +03:00
case pb . Access_UNKNOWN :
2020-03-26 13:12:43 +00: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-02 17:01:06 +01:00
log . Infof ( "%v:%v granted access to %v:%v:%v:%v by rule %v" , acc . Namespace , logID , res . Namespace , res . Type , res . Name , res . Endpoint , rule . Id )
2020-03-26 13:12:43 +00: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-02 17:01:06 +01:00
log . Infof ( "%v:%v denied access to %v:%v:%v:%v by rule %v" , acc . Namespace , logID , res . Namespace , res . Type , res . Name , res . Endpoint , rule . Id )
2020-03-26 17:35:28 +00:00
return auth . ErrForbidden // rule denies access to the resource
2020-03-24 13:48:37 +00:00
}
}
}
2020-03-26 13:12:43 +00:00
// no rules were found for the resource, default to denying access
2020-04-02 17:01:06 +01:00
log . Infof ( "%v:%v denied access to %v:%v:%v:%v by lack of rule (%v rules found for namespace)" , acc . Namespace , logID , res . Namespace , res . Type , res . Name , res . Endpoint , len ( s . listRules ( res . Namespace ) ) )
2020-03-24 13:48:37 +00:00
return auth . ErrForbidden
2020-03-23 16:19:30 +00:00
}
// Inspect a token
func ( s * svc ) Inspect ( token string ) ( * auth . Account , error ) {
2020-04-01 14:25:00 +01:00
// try to decode JWT locally and fall back to srv if an error occurs
2020-03-23 16:19:30 +00:00
if len ( strings . Split ( token , "." ) ) == 3 && s . jwt != nil {
2020-04-01 14:25:00 +01:00
if acc , err := s . jwt . Inspect ( token ) ; err == nil {
return acc , nil
2020-03-23 16:19:30 +00:00
}
}
2020-04-01 14:25:00 +01:00
rsp , err := s . auth . Inspect ( context . TODO ( ) , & pb . InspectRequest { Token : token } )
2020-02-03 08:16:02 +00:00
if err != nil {
return nil , err
}
2020-03-23 16:19:30 +00:00
return serializeAccount ( rsp . Account ) , nil
2020-02-03 08:16:02 +00:00
}
2020-03-31 10:06:13 +01:00
// Token generation using an account ID and secret
2020-04-01 14:25:00 +01:00
func ( s * svc ) Token ( opts ... auth . TokenOption ) ( * auth . Token , error ) {
2020-03-31 10:06:13 +01:00
options := auth . NewTokenOptions ( opts ... )
2020-02-03 08:16:02 +00:00
2020-03-31 10:06:13 +01:00
rsp , err := s . auth . Token ( context . Background ( ) , & pb . TokenRequest {
2020-04-01 14:25:00 +01:00
Id : options . ID ,
Secret : options . Secret ,
RefreshToken : options . RefreshToken ,
TokenExpiry : int64 ( options . Expiry . Seconds ( ) ) ,
2020-03-23 16:19:30 +00:00
} )
if err != nil {
return nil , err
2020-02-03 08:16:02 +00:00
}
2020-03-23 16:19:30 +00:00
return serializeToken ( rsp . Token ) , nil
2020-02-03 08:16:02 +00:00
}
2020-03-24 13:48:37 +00:00
var ruleJoinKey = ":"
2020-03-26 13:12:43 +00:00
// accessForRule returns a rule status, indicating if a rule permits access to a
2020-03-24 13:48:37 +00: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-03-24 13:48:37 +00:00
if rule . Role == "*" {
2020-03-26 13:12:43 +00:00
return rule . Access
2020-03-24 13:48:37 +00:00
}
for _ , role := range acc . Roles {
if rule . Role == role {
2020-03-26 13:12:43 +00:00
return rule . Access
2020-03-24 13:48:37 +00:00
}
// allow user.anything if role is user.*
if strings . HasSuffix ( rule . Role , ".*" ) && strings . HasPrefix ( rule . Role , role + "." ) {
2020-03-26 13:12:43 +00:00
return rule . Access
2020-03-24 13:48:37 +00:00
}
}
2020-03-30 18:23:00 +03:00
return pb . Access_UNKNOWN
2020-03-24 13:48:37 +00:00
}
2020-04-02 17:01:06 +01: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-03-24 13:48:37 +00:00
s . Lock ( )
defer s . Unlock ( )
2020-03-30 18:23:00 +03:00
var rules [ ] * pb . Rule
2020-03-24 13:48:37 +00:00
for _ , r := range s . rules {
2020-04-02 17:01:06 +01:00
if len ( filters ) > 0 && r . Resource . Namespace != filters [ 0 ] {
continue
}
if len ( filters ) > 1 && r . Resource . Type != filters [ 1 ] {
continue
2020-03-24 13:48:37 +00:00
}
2020-04-02 17:01:06 +01: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 13:48:37 +00:00
}
return rules
}
// loadRules retrieves the rules from the auth service
func ( s * svc ) loadRules ( ) {
2020-03-30 18:23:00 +03:00
rsp , err := s . rule . List ( context . TODO ( ) , & pb . ListRequest { } )
2020-03-24 13:48:37 +00:00
s . Lock ( )
defer s . Unlock ( )
if err != nil {
log . Errorf ( "Error listing rules: %v" , err )
return
}
s . rules = rsp . Rules
}
2020-04-01 14:25:00 +01:00
// refreshToken generates a new token for the service to use when making calls
func ( s * svc ) refreshToken ( ) {
req := & pb . TokenRequest {
TokenExpiry : int64 ( ( time . Minute * 15 ) . Seconds ( ) ) ,
}
if s . Options ( ) . Token == nil {
// we do not have a token, use the credentials to get one
req . Id = s . Options ( ) . ID
req . Secret = s . Options ( ) . Secret
} else {
// we have a token, refresh it
req . RefreshToken = s . Options ( ) . Token . RefreshToken
}
rsp , err := s . auth . Token ( context . TODO ( ) , req )
2020-03-31 12:44:34 +01:00
s . Lock ( )
defer s . Unlock ( )
if err != nil {
log . Errorf ( "Error generating token: %v" , err )
return
}
s . options . Token = serializeToken ( rsp . Token )
}
2020-03-30 18:23:00 +03:00
func serializeToken ( t * pb . Token ) * auth . Token {
2020-03-23 16:19:30 +00:00
return & auth . Token {
2020-04-01 14:25:00 +01:00
AccessToken : t . AccessToken ,
RefreshToken : t . RefreshToken ,
Created : time . Unix ( t . Created , 0 ) ,
Expiry : time . Unix ( t . Expiry , 0 ) ,
2020-02-03 08:16:02 +00:00
}
2020-03-23 16:19:30 +00:00
}
2020-02-03 08:16:02 +00:00
2020-03-30 18:23:00 +03:00
func serializeAccount ( a * pb . Account ) * auth . Account {
2020-03-23 16:19:30 +00:00
return & auth . Account {
2020-04-01 14:25:00 +01:00
ID : a . Id ,
Roles : a . Roles ,
2020-04-01 17:20:02 +01:00
Secret : a . Secret ,
2020-04-01 14:25:00 +01:00
Metadata : a . Metadata ,
Provider : a . Provider ,
Namespace : a . Namespace ,
2020-03-23 16:19:30 +00:00
}
2020-02-03 08:16:02 +00:00
}