195 lines
5.5 KiB
Go
195 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
userpb "juwan-backend/app/users/rpc/pb"
|
|
|
|
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
|
authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
|
|
typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
|
|
"github.com/zeromicro/go-zero/core/logx"
|
|
codepb "google.golang.org/genproto/googleapis/rpc/code"
|
|
statuspb "google.golang.org/genproto/googleapis/rpc/status"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
const (
|
|
headerAuthUserID = "x-auth-user-id"
|
|
headerAuthRoleType = "x-auth-role-type"
|
|
headerAuthIsAdmin = "x-auth-is-admin"
|
|
headerCookie = "cookie"
|
|
cookieJToken = "JToken"
|
|
)
|
|
|
|
type authzServer struct {
|
|
authv3.UnimplementedAuthorizationServer
|
|
userRPC userpb.UsercenterClient
|
|
}
|
|
|
|
func (s *authzServer) Check(ctx context.Context, req *authv3.CheckRequest) (*authv3.CheckResponse, error) {
|
|
httpReq := req.GetAttributes().GetRequest().GetHttp()
|
|
if httpReq == nil {
|
|
return deny(codepb.Code_INVALID_ARGUMENT, typev3.StatusCode_BadRequest, "missing http attributes"), nil
|
|
}
|
|
|
|
path := httpReq.GetPath()
|
|
if isPublicPath(path) {
|
|
return allow(nil), nil
|
|
}
|
|
|
|
token, ok := getCookieValue(httpReq.GetHeaders(), cookieJToken)
|
|
if !ok || token == "" {
|
|
return deny(codepb.Code_UNAUTHENTICATED, typev3.StatusCode_Unauthorized, "missing JToken cookie"), nil
|
|
}
|
|
|
|
userIDHeader := getHeader(httpReq.GetHeaders(), headerAuthUserID)
|
|
if userIDHeader == "" {
|
|
return deny(codepb.Code_UNAUTHENTICATED, typev3.StatusCode_Unauthorized, "missing x-auth-user-id header"), nil
|
|
}
|
|
|
|
userID, err := strconv.ParseInt(userIDHeader, 10, 64)
|
|
if err != nil || userID <= 0 {
|
|
return deny(codepb.Code_UNAUTHENTICATED, typev3.StatusCode_Unauthorized, "invalid x-auth-user-id"), nil
|
|
}
|
|
|
|
rpcCtx, cancel := context.WithTimeout(ctx, 1200*time.Millisecond)
|
|
defer cancel()
|
|
|
|
resp, err := s.userRPC.ValidateToken(rpcCtx, &userpb.ValidateTokenReq{
|
|
Token: token,
|
|
UserId: userID,
|
|
})
|
|
if err != nil {
|
|
logx.Infof("validate token rpc failed, err: %v", err)
|
|
return deny(codepb.Code_UNAUTHENTICATED, typev3.StatusCode_Unauthorized, "validate token failed"), nil
|
|
}
|
|
if !resp.GetValid() {
|
|
logx.Infof("validate token rpc failed, err: %v", err)
|
|
return deny(codepb.Code_PERMISSION_DENIED, typev3.StatusCode_Forbidden, "token invalid"), nil
|
|
}
|
|
|
|
outHeaders := []*corev3.HeaderValueOption{
|
|
{Header: &corev3.HeaderValue{Key: headerAuthUserID, Value: strconv.FormatInt(resp.GetUserId(), 10)}},
|
|
{Header: &corev3.HeaderValue{Key: headerAuthRoleType, Value: resp.GetRoleType()}},
|
|
}
|
|
|
|
if getHeader(httpReq.GetHeaders(), headerAuthIsAdmin) != "" {
|
|
outHeaders = append(outHeaders, &corev3.HeaderValueOption{Header: &corev3.HeaderValue{Key: headerAuthIsAdmin, Value: getHeader(httpReq.GetHeaders(), headerAuthIsAdmin)}})
|
|
}
|
|
|
|
return allow(outHeaders), nil
|
|
}
|
|
|
|
func allow(headers []*corev3.HeaderValueOption) *authv3.CheckResponse {
|
|
return &authv3.CheckResponse{
|
|
Status: &statuspb.Status{Code: int32(codepb.Code_OK)},
|
|
HttpResponse: &authv3.CheckResponse_OkResponse{
|
|
OkResponse: &authv3.OkHttpResponse{Headers: headers},
|
|
},
|
|
}
|
|
}
|
|
|
|
func deny(code codepb.Code, httpCode typev3.StatusCode, message string) *authv3.CheckResponse {
|
|
return &authv3.CheckResponse{
|
|
Status: &statuspb.Status{Code: int32(code), Message: message},
|
|
HttpResponse: &authv3.CheckResponse_DeniedResponse{
|
|
DeniedResponse: &authv3.DeniedHttpResponse{
|
|
Status: &typev3.HttpStatus{Code: httpCode},
|
|
Body: fmt.Sprintf(`{"code":%d,"message":"%s"}`, httpCode, message),
|
|
Headers: []*corev3.HeaderValueOption{
|
|
{Header: &corev3.HeaderValue{Key: "content-type", Value: "application/json"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func isPublicPath(path string) bool {
|
|
switch path {
|
|
case "/healthz",
|
|
"/api/v1/auth/login",
|
|
"/api/v1/auth/register",
|
|
"/api/v1/auth/forgot-password",
|
|
"/api/v1/auth/reset-password",
|
|
"/api/v1/auth/forgot-password/send",
|
|
"/api/v1/email/verification-code/send":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func getHeader(headers map[string]string, key string) string {
|
|
for k, v := range headers {
|
|
if strings.EqualFold(k, key) {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getCookieValue(headers map[string]string, name string) (string, bool) {
|
|
cookieHeader := getHeader(headers, headerCookie)
|
|
if cookieHeader == "" {
|
|
return "", false
|
|
}
|
|
parts := strings.Split(cookieHeader, ";")
|
|
for _, part := range parts {
|
|
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
|
|
if len(kv) != 2 {
|
|
continue
|
|
}
|
|
if kv[0] == name {
|
|
return kv[1], true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func getEnvWithDefault(key, defaultValue string) string {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return defaultValue
|
|
}
|
|
return value
|
|
}
|
|
|
|
func run() error {
|
|
listenOn := getEnvWithDefault("LISTEN_ON", "0.0.0.0:9002")
|
|
userRPCTarget := getEnvWithDefault("USER_RPC_TARGET", "user-rpc-svc.juwan.svc.cluster.local:9001")
|
|
|
|
conn, err := grpc.NewClient(userRPCTarget, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
if err != nil {
|
|
return fmt.Errorf("dial user rpc failed: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
lis, err := net.Listen("tcp", listenOn)
|
|
if err != nil {
|
|
return fmt.Errorf("listen failed: %w", err)
|
|
}
|
|
|
|
grpcServer := grpc.NewServer()
|
|
authv3.RegisterAuthorizationServer(grpcServer, &authzServer{userRPC: userpb.NewUsercenterClient(conn)})
|
|
|
|
fmt.Printf("authz-adapter listening on %s, user-rpc target %s\n", listenOn, userRPCTarget)
|
|
return grpcServer.Serve(lis)
|
|
}
|
|
|
|
func main() {
|
|
if err := run(); err != nil {
|
|
if !errors.Is(err, net.ErrClosed) {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|