feat: add some api packages

This commit is contained in:
Livio Amstutz
2020-03-23 07:01:59 +01:00
parent 021ee07350
commit c89397e1b4
19 changed files with 1535 additions and 190 deletions

View File

@@ -0,0 +1,108 @@
package auth
import (
"context"
"fmt"
"reflect"
"strings"
"github.com/caos/zitadel/internal/errors"
)
const (
authenticated = "authenticated"
)
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID string, verifier TokenVerifier, authConfig *Config, requiredAuthOption Option) (context.Context, error) {
ctx, err := VerifyTokenAndWriteCtxData(ctx, token, orgID, verifier)
if err != nil {
return nil, err
}
if requiredAuthOption.Permission == authenticated {
return ctx, nil
}
ctx, perms, err := getUserMethodPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig)
if err != nil {
return nil, err
}
err = checkUserPermissions(req, perms, requiredAuthOption)
if err != nil {
return nil, err
}
return ctx, nil
}
func checkUserPermissions(req interface{}, userPerms []string, authOpt Option) error {
if len(userPerms) == 0 {
return errors.ThrowPermissionDenied(nil, "AUTH-5mWD2", "No matching permissions found")
}
if authOpt.CheckParam == "" {
return nil
}
if HasGlobalPermission(userPerms) {
return nil
}
if hasContextPermission(req, authOpt.CheckParam, userPerms) {
return nil
}
return errors.ThrowPermissionDenied(nil, "AUTH-3jknH", "No matching permissions found")
}
func SplitPermission(perm string) (string, string) {
splittedPerm := strings.Split(perm, ":")
if len(splittedPerm) == 1 {
return splittedPerm[0], ""
}
return splittedPerm[0], splittedPerm[1]
}
func hasContextPermission(req interface{}, fieldName string, permissions []string) bool {
for _, perm := range permissions {
_, ctxID := SplitPermission(perm)
if checkPermissionContext(req, fieldName, ctxID) {
return true
}
}
return false
}
func checkPermissionContext(req interface{}, fieldName, roleContextID string) bool {
field := getFieldFromReq(req, fieldName)
return field != "" && field == roleContextID
}
func getFieldFromReq(req interface{}, field string) string {
v := reflect.Indirect(reflect.ValueOf(req)).FieldByName(field)
if reflect.ValueOf(v).IsZero() {
return ""
}
return fmt.Sprintf("%v", v.Interface())
}
func HasGlobalPermission(perms []string) bool {
for _, perm := range perms {
_, ctxID := SplitPermission(perm)
if ctxID == "" {
return true
}
}
return false
}
func GetPermissionCtxIDs(perms []string) []string {
ctxIDs := make([]string, 0)
for _, perm := range perms {
_, ctxID := SplitPermission(perm)
if ctxID != "" {
ctxIDs = append(ctxIDs, ctxID)
}
}
return ctxIDs
}

View File

@@ -0,0 +1,280 @@
package auth
import (
"testing"
"github.com/caos/zitadel/internal/errors"
)
type TestRequest struct {
Test string
}
func Test_CheckUserPermissions(t *testing.T) {
type args struct {
req *TestRequest
perms []string
authOpt Option
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "no permissions",
args: args{
req: &TestRequest{},
perms: []string{},
},
wantErr: true,
},
{
name: "has permission and no context requested",
args: args{
req: &TestRequest{},
perms: []string{"project.read"},
authOpt: Option{CheckParam: ""},
},
wantErr: false,
},
{
name: "context requested and has global permission",
args: args{
req: &TestRequest{Test: "Test"},
perms: []string{"project.read", "project.read:1"},
authOpt: Option{CheckParam: "Test"},
},
wantErr: false,
},
{
name: "context requested and has specific permission",
args: args{
req: &TestRequest{Test: "Test"},
perms: []string{"project.read:Test"},
authOpt: Option{CheckParam: "Test"},
},
wantErr: false,
},
{
name: "context requested and has no permission",
args: args{
req: &TestRequest{Test: "Hodor"},
perms: []string{"project.read:Test"},
authOpt: Option{CheckParam: "Test"},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkUserPermissions(tt.args.req, tt.args.perms, tt.args.authOpt)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
}
if !tt.wantErr && err != nil {
t.Errorf("shouldn't get err: %v ", err)
}
if tt.wantErr && !errors.IsPermissionDenied(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func Test_SplitPermission(t *testing.T) {
type args struct {
perm string
}
tests := []struct {
name string
args args
permName string
permCtxID string
}{
{
name: "permission with context id",
args: args{
perm: "project.read:ctxID",
},
permName: "project.read",
permCtxID: "ctxID",
},
{
name: "permission without context id",
args: args{
perm: "project.read",
},
permName: "project.read",
permCtxID: "",
},
{
name: "permission to many parts",
args: args{
perm: "project.read:1:0",
},
permName: "project.read",
permCtxID: "1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name, id := SplitPermission(tt.args.perm)
if name != tt.permName {
t.Errorf("got wrong result on name, expecting: %v, actual: %v ", tt.permName, name)
}
if id != tt.permCtxID {
t.Errorf("got wrong result on id, expecting: %v, actual: %v ", tt.permCtxID, id)
}
})
}
}
func Test_HasContextPermission(t *testing.T) {
type args struct {
req *TestRequest
fieldname string
perms []string
}
tests := []struct {
name string
args args
result bool
}{
{
name: "existing context permission",
args: args{
req: &TestRequest{Test: "right"},
fieldname: "Test",
perms: []string{"test:wrong", "test:right"},
},
result: true,
},
{
name: "not existing context permission",
args: args{
req: &TestRequest{Test: "test"},
fieldname: "Test",
perms: []string{"test:wrong", "test:wrong2"},
},
result: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hasContextPermission(tt.args.req, tt.args.fieldname, tt.args.perms)
if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}
func Test_GetFieldFromReq(t *testing.T) {
type args struct {
req *TestRequest
fieldname string
}
tests := []struct {
name string
args args
result string
}{
{
name: "existing field",
args: args{
req: &TestRequest{Test: "TestValue"},
fieldname: "Test",
},
result: "TestValue",
},
{
name: "not existing field",
args: args{
req: &TestRequest{Test: "TestValue"},
fieldname: "Test2",
},
result: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getFieldFromReq(tt.args.req, tt.args.fieldname)
if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}
func Test_HasGlobalPermission(t *testing.T) {
type args struct {
perms []string
}
tests := []struct {
name string
args args
result bool
}{
{
name: "global perm existing",
args: args{
perms: []string{"perm:1", "perm:2", "perm"},
},
result: true,
},
{
name: "global perm not existing",
args: args{
perms: []string{"perm:1", "perm:2", "perm:3"},
},
result: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := HasGlobalPermission(tt.args.perms)
if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}
func Test_GetPermissionCtxIDs(t *testing.T) {
type args struct {
perms []string
}
tests := []struct {
name string
args args
result []string
}{
{
name: "no specific permission",
args: args{
perms: []string{"perm"},
},
result: []string{},
},
{
name: "ctx id",
args: args{
perms: []string{"perm:1", "perm", "perm:3"},
},
result: []string{"1", "3"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetPermissionCtxIDs(tt.args.perms)
if !EqualStringArray(result, tt.result) {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}

View File

@@ -0,0 +1,26 @@
package auth
type Config struct {
RolePermissionMappings []RoleMapping
}
type RoleMapping struct {
Role string
Permissions []string
}
type MethodMapping map[string]Option
type Option struct {
Permission string
CheckParam string
}
func (a *Config) getPermissionsFromRole(role string) []string {
for _, roleMap := range a.RolePermissionMappings {
if roleMap.Role == role {
return roleMap.Permissions
}
}
return nil
}

View File

@@ -0,0 +1,70 @@
package auth
import (
"context"
"time"
"github.com/caos/logging"
)
const (
HeaderOrgID = "x-caos-zitadel-orgid"
)
type CtxKeyPermissions struct{}
type CtxKeyData struct{}
type CtxData struct {
UserID string
OrgID string
ProjectID string
AgentID string
}
func (ctxData CtxData) IsZero() bool {
return ctxData.UserID == "" || ctxData.OrgID == ""
}
type Grants []*Grant
type Grant struct {
OrgID string
Roles []string
}
type TokenVerifier interface {
VerifyAccessToken(ctx context.Context, token string) (string, string, string, error)
ResolveGrants(ctx context.Context, sub, orgID string) ([]*Grant, error)
GetProjectIDByClientID(ctx context.Context, clientID string) (string, error)
}
func VerifyTokenAndWriteCtxData(ctx context.Context, token, orgID string, t TokenVerifier) (_ context.Context, err error) {
userID, clientID, agentID, err := verifyAccessToken(ctx, token, t)
if err != nil {
return nil, err
}
projectID, err := t.GetProjectIDByClientID(ctx, clientID)
logging.LogWithFields("AUTH-GfAoV", "clientID", clientID).OnError(err).Warn("could not read projectid by clientid")
return context.WithValue(ctx, CtxKeyData{}, &CtxData{UserID: userID, OrgID: orgID, ProjectID: projectID, AgentID: agentID}), nil
}
func GetCtxData(ctx context.Context) CtxData {
if data := ctx.Value(CtxKeyData{}); data != nil {
ctxData, ok := data.(*CtxData)
if ok {
return *ctxData
}
time.Now()
}
return CtxData{}
}
func GetPermissionsFromCtx(ctx context.Context) []string {
if data := ctx.Value(CtxKeyPermissions{}); data != nil {
ctxPermission, _ := data.([]string)
return ctxPermission
}
return nil
}

View File

@@ -0,0 +1,61 @@
package auth
import (
"context"
"github.com/caos/zitadel/internal/errors"
)
func getUserMethodPermissions(ctx context.Context, t TokenVerifier, requiredPerm string, authConfig *Config) (context.Context, []string, error) {
ctxData := GetCtxData(ctx)
if ctxData.IsZero() {
return nil, nil, errors.ThrowUnauthenticated(nil, "AUTH-rKLWEH", "context missing")
}
grants, err := t.ResolveGrants(ctx, ctxData.UserID, ctxData.OrgID)
if err != nil {
return nil, nil, err
}
permissions := mapGrantsToPermissions(requiredPerm, grants, authConfig)
return context.WithValue(ctx, CtxKeyPermissions{}, permissions), permissions, nil
}
func mapGrantsToPermissions(requiredPerm string, grants []*Grant, authConfig *Config) []string {
resolvedPermissions := make([]string, 0)
for _, grant := range grants {
for _, role := range grant.Roles {
resolvedPermissions = mapRoleToPerm(requiredPerm, role, authConfig, resolvedPermissions)
}
}
return resolvedPermissions
}
func mapRoleToPerm(requiredPerm, actualRole string, authConfig *Config, resolvedPermissions []string) []string {
roleName, roleContextID := SplitPermission(actualRole)
perms := authConfig.getPermissionsFromRole(roleName)
for _, p := range perms {
if p == requiredPerm {
p = addRoleContextIDToPerm(p, roleContextID)
if !existsPerm(resolvedPermissions, p) {
resolvedPermissions = append(resolvedPermissions, p)
}
}
}
return resolvedPermissions
}
func addRoleContextIDToPerm(perm, roleContextID string) string {
if roleContextID != "" {
perm = perm + ":" + roleContextID
}
return perm
}
func existsPerm(existing []string, perm string) bool {
for _, e := range existing {
if e == perm {
return true
}
}
return false
}

View File

@@ -0,0 +1,431 @@
package auth
import (
"context"
"testing"
caos_errs "github.com/caos/zitadel/internal/errors"
)
func getTestCtx(userID, orgID string) context.Context {
return context.WithValue(context.Background(), CtxKeyData{}, &CtxData{UserID: userID, OrgID: orgID})
}
type testVerifier struct {
grants []*Grant
}
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token string) (string, string, string, error) {
return "", "", "", nil
}
func (v *testVerifier) ResolveGrants(ctx context.Context, sub, orgID string) ([]*Grant, error) {
return v.grants, nil
}
func (v *testVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) {
return "", nil
}
func EqualStringArray(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
func Test_GetUserMethodPermissions(t *testing.T) {
type args struct {
ctx context.Context
verifier TokenVerifier
requiredPerm string
authConfig *Config
}
tests := []struct {
name string
args args
wantErr bool
errFunc func(err error) bool
result []string
}{
{
name: "Empty Context",
args: args{
ctx: getTestCtx("", ""),
verifier: &testVerifier{grants: []*Grant{&Grant{
Roles: []string{"ORG_OWNER"}}}},
requiredPerm: "project.read",
authConfig: &Config{
RolePermissionMappings: []RoleMapping{
RoleMapping{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
RoleMapping{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
wantErr: true,
errFunc: func(err error) bool {
return caos_errs.IsUnauthenticated(err)
},
result: []string{"project.read"},
},
{
name: "No Grants",
args: args{
ctx: getTestCtx("", ""),
verifier: &testVerifier{grants: []*Grant{}},
requiredPerm: "project.read",
authConfig: &Config{
RolePermissionMappings: []RoleMapping{
RoleMapping{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
RoleMapping{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
result: make([]string, 0),
},
{
name: "Get Permissions",
args: args{
ctx: getTestCtx("userID", "orgID"),
verifier: &testVerifier{grants: []*Grant{&Grant{
Roles: []string{"ORG_OWNER"}}}},
requiredPerm: "project.read",
authConfig: &Config{
RolePermissionMappings: []RoleMapping{
RoleMapping{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
RoleMapping{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
result: []string{"project.read"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, perms, err := getUserMethodPermissions(tt.args.ctx, tt.args.verifier, tt.args.requiredPerm, tt.args.authConfig)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
}
if tt.wantErr && !tt.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
if !tt.wantErr && !EqualStringArray(perms, tt.result) {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, perms)
}
})
}
}
func Test_MapGrantsToPermissions(t *testing.T) {
type args struct {
requiredPerm string
grants []*Grant
authConfig *Config
}
tests := []struct {
name string
args args
result []string
}{
{
name: "One Role existing perm",
args: args{
requiredPerm: "project.read",
grants: []*Grant{&Grant{
Roles: []string{"ORG_OWNER"}}},
authConfig: &Config{
RolePermissionMappings: []RoleMapping{
RoleMapping{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
RoleMapping{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
result: []string{"project.read"},
},
{
name: "One Role not existing perm",
args: args{
requiredPerm: "project.write",
grants: []*Grant{&Grant{
Roles: []string{"ORG_OWNER"}}},
authConfig: &Config{
RolePermissionMappings: []RoleMapping{
RoleMapping{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
RoleMapping{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
result: []string{},
},
{
name: "Multiple Roles one existing",
args: args{
requiredPerm: "project.read",
grants: []*Grant{&Grant{
Roles: []string{"ORG_OWNER", "IAM_OWNER"}}},
authConfig: &Config{
RolePermissionMappings: []RoleMapping{
RoleMapping{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
RoleMapping{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
result: []string{"project.read"},
},
{
name: "Multiple Roles, global and specific",
args: args{
requiredPerm: "project.read",
grants: []*Grant{&Grant{
Roles: []string{"ORG_OWNER", "PROJECT_OWNER:1"}}},
authConfig: &Config{
RolePermissionMappings: []RoleMapping{
RoleMapping{
Role: "PROJECT_OWNER",
Permissions: []string{"project.read"},
},
RoleMapping{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
result: []string{"project.read", "project.read:1"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := mapGrantsToPermissions(tt.args.requiredPerm, tt.args.grants, tt.args.authConfig)
if !EqualStringArray(result, tt.result) {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}
func Test_MapRoleToPerm(t *testing.T) {
type args struct {
requiredPerm string
actualRole string
authConfig *Config
resolvedPermissions []string
}
tests := []struct {
name string
args args
result []string
}{
{
name: "first perm without context id",
args: args{
requiredPerm: "project.read",
actualRole: "ORG_OWNER",
authConfig: &Config{
RolePermissionMappings: []RoleMapping{
RoleMapping{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
RoleMapping{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
resolvedPermissions: []string{},
},
result: []string{"project.read"},
},
{
name: "existing perm without context id",
args: args{
requiredPerm: "project.read",
actualRole: "ORG_OWNER",
authConfig: &Config{
RolePermissionMappings: []RoleMapping{
RoleMapping{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
RoleMapping{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
resolvedPermissions: []string{"project.read"},
},
result: []string{"project.read"},
},
{
name: "first perm with context id",
args: args{
requiredPerm: "project.read",
actualRole: "PROJECT_OWNER:1",
authConfig: &Config{
RolePermissionMappings: []RoleMapping{
RoleMapping{
Role: "PROJECT_OWNER",
Permissions: []string{"project.read"},
},
RoleMapping{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
resolvedPermissions: []string{},
},
result: []string{"project.read:1"},
},
{
name: "perm with context id, existing global",
args: args{
requiredPerm: "project.read",
actualRole: "PROJECT_OWNER:1",
authConfig: &Config{
RolePermissionMappings: []RoleMapping{
RoleMapping{
Role: "PROJECT_OWNER",
Permissions: []string{"project.read"},
},
RoleMapping{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
resolvedPermissions: []string{"project.read"},
},
result: []string{"project.read", "project.read:1"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := mapRoleToPerm(tt.args.requiredPerm, tt.args.actualRole, tt.args.authConfig, tt.args.resolvedPermissions)
if !EqualStringArray(result, tt.result) {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}
func Test_AddRoleContextIDToPerm(t *testing.T) {
type args struct {
perm string
ctxID string
}
tests := []struct {
name string
args args
result string
}{
{
name: "with ctx id",
args: args{
perm: "perm1",
ctxID: "2",
},
result: "perm1:2",
},
{
name: "with ctx id",
args: args{
perm: "perm1",
ctxID: "",
},
result: "perm1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := addRoleContextIDToPerm(tt.args.perm, tt.args.ctxID)
if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}
func Test_ExistisPerm(t *testing.T) {
type args struct {
existing []string
perm string
}
tests := []struct {
name string
args args
result bool
}{
{
name: "not existing perm",
args: args{
existing: []string{"perm1", "perm2", "perm3"},
perm: "perm4",
},
result: false,
},
{
name: "existing perm",
args: args{
existing: []string{"perm1", "perm2", "perm3"},
perm: "perm2",
},
result: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := existsPerm(tt.args.existing, tt.args.perm)
if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}

View File

@@ -0,0 +1,20 @@
package auth
import (
"context"
"strings"
"github.com/caos/zitadel/internal/errors"
)
const (
BearerPrefix = "Bearer "
)
func verifyAccessToken(ctx context.Context, token string, t TokenVerifier) (string, string, string, error) {
parts := strings.Split(token, BearerPrefix)
if len(parts) != 2 {
return "", "", "", errors.ThrowUnauthenticated(nil, "AUTH-7fs1e", "invalid auth header")
}
return t.VerifyAccessToken(ctx, parts[1])
}

View File

@@ -0,0 +1,63 @@
package auth
import (
"context"
"testing"
"github.com/caos/zitadel/internal/errors"
)
func Test_VerifyAccessToken(t *testing.T) {
type args struct {
ctx context.Context
token string
verifier *testVerifier
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "no auth header set",
args: args{
ctx: context.Background(),
token: "",
},
wantErr: true,
},
{
name: "wrong auth header set",
args: args{
ctx: context.Background(),
token: "Basic sds",
},
wantErr: true,
},
{
name: "auth header set",
args: args{
ctx: context.Background(),
token: "Bearer AUTH",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, _, _, err := verifyAccessToken(tt.args.ctx, tt.args.token, tt.args.verifier)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
}
if !tt.wantErr && err != nil {
t.Errorf("got wrong result, should not get err: actual: %v ", err)
}
if tt.wantErr && !errors.IsUnauthenticated(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}

View File

@@ -0,0 +1,34 @@
package grpc
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/caos/zitadel/internal/api/auth"
)
func AuthorizationInterceptor(verifier auth.TokenVerifier, authConfig *auth.Config, authMethods auth.MethodMapping) func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
authOpt, needsToken := authMethods[info.FullMethod]
if !needsToken {
return handler(ctx, req)
}
authToken := GetAuthorizationHeader(ctx)
if authToken == "" {
return nil, status.Error(codes.Unauthenticated, "auth header missing")
}
orgID := GetHeader(ctx, ZitadelOrgID)
ctx, err := auth.CheckUserAuthorization(ctx, req, authToken, orgID, verifier, authConfig, authOpt)
if err != nil {
return nil, err
}
return handler(ctx, req)
}
}

View File

@@ -0,0 +1,46 @@
package grpc
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
caos_errs "github.com/caos/zitadel/internal/errors"
)
func CaosToGRPCError(err error) error {
if err == nil {
return nil
}
code, msg, ok := extract(err)
if !ok {
return status.Convert(err).Err()
}
return status.Error(code, msg)
}
func extract(err error) (c codes.Code, msg string, ok bool) {
switch caosErr := err.(type) {
case *caos_errs.AlreadyExistsError:
return codes.AlreadyExists, caosErr.GetMessage(), true
case *caos_errs.DeadlineExceededError:
return codes.DeadlineExceeded, caosErr.GetMessage(), true
case caos_errs.InternalError:
return codes.Internal, caosErr.GetMessage(), true
case *caos_errs.InvalidArgumentError:
return codes.InvalidArgument, caosErr.GetMessage(), true
case *caos_errs.NotFoundError:
return codes.NotFound, caosErr.GetMessage(), true
case *caos_errs.PermissionDeniedError:
return codes.PermissionDenied, caosErr.GetMessage(), true
case *caos_errs.PreconditionFailedError:
return codes.FailedPrecondition, caosErr.GetMessage(), true
case *caos_errs.UnauthenticatedError:
return codes.Unauthenticated, caosErr.GetMessage(), true
case *caos_errs.UnavailableError:
return codes.Unavailable, caosErr.GetMessage(), true
case *caos_errs.UnimplementedError:
return codes.Unimplemented, caosErr.GetMessage(), true
default:
return codes.Unknown, err.Error(), false
}
}

View File

@@ -0,0 +1,14 @@
package grpc
import (
"context"
"google.golang.org/grpc"
)
func ErrorHandler() func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
resp, err := handler(ctx, req)
return resp, CaosToGRPCError(err)
}
}

View File

@@ -0,0 +1,21 @@
package grpc
import (
"context"
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
)
const (
Authorization = "authorization"
ZitadelOrgID = "x-zitadel-orgid"
)
func GetHeader(ctx context.Context, headername string) string {
return metautils.ExtractIncoming(ctx).Get(headername)
}
func GetAuthorizationHeader(ctx context.Context) string {
return GetHeader(ctx, Authorization)
}

View File

@@ -0,0 +1,53 @@
package grpc
import (
"context"
"github.com/caos/logging"
"github.com/golang/protobuf/ptypes/empty"
structpb "github.com/golang/protobuf/ptypes/struct"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/proto"
)
type ValidationFunction func(ctx context.Context) error
type Validator struct {
validations map[string]ValidationFunction
}
func NewValidator(validations map[string]ValidationFunction) *Validator {
return &Validator{validations: validations}
}
func (v *Validator) Healthz(_ context.Context, e *empty.Empty) (*empty.Empty, error) {
return e, nil
}
func (v *Validator) Ready(ctx context.Context, e *empty.Empty) (*empty.Empty, error) {
return e, ready(ctx, v.validations)
}
func (v *Validator) Validate(ctx context.Context, _ *empty.Empty) (*structpb.Struct, error) {
validations := validate(ctx, v.validations)
return proto.ToPBStruct(validations)
}
func ready(ctx context.Context, validations map[string]ValidationFunction) error {
if len(validate(ctx, validations)) == 0 {
return nil
}
return errors.ThrowInternal(nil, "API-2jD9a", "not ready")
}
func validate(ctx context.Context, validations map[string]ValidationFunction) map[string]error {
errors := make(map[string]error)
for id, validation := range validations {
if err := validation(ctx); err != nil {
logging.Log("API-vf823").WithError(err).Error("validation failed")
errors[id] = err
}
}
return errors
}

125
internal/api/http/cookie.go Normal file
View File

@@ -0,0 +1,125 @@
package http
import (
"net/http"
"github.com/gorilla/securecookie"
"github.com/caos/zitadel/internal/errors"
)
type CookieHandler struct {
securecookie *securecookie.SecureCookie
secureOnly bool
sameSite http.SameSite
path string
maxAge int
domain string
}
func NewCookieHandler(opts ...CookieHandlerOpt) *CookieHandler {
c := &CookieHandler{
secureOnly: true,
sameSite: http.SameSiteLaxMode,
path: "/",
}
for _, opt := range opts {
opt(c)
}
return c
}
type CookieHandlerOpt func(*CookieHandler)
func WithEncryption(hashKey, encryptKey []byte) CookieHandlerOpt {
return func(c *CookieHandler) {
c.securecookie = securecookie.New(hashKey, encryptKey)
}
}
func WithUnsecure() CookieHandlerOpt {
return func(c *CookieHandler) {
c.secureOnly = false
}
}
func WithSameSite(sameSite http.SameSite) CookieHandlerOpt {
return func(c *CookieHandler) {
c.sameSite = sameSite
}
}
func WithPath(path string) CookieHandlerOpt {
return func(c *CookieHandler) {
c.path = path
}
}
func WithMaxAge(maxAge int) CookieHandlerOpt {
return func(c *CookieHandler) {
c.maxAge = maxAge
c.securecookie.MaxAge(maxAge)
}
}
func WithDomain(domain string) CookieHandlerOpt {
return func(c *CookieHandler) {
c.domain = domain
}
}
func (c *CookieHandler) GetCookieValue(r *http.Request, name string) (string, error) {
cookie, err := r.Cookie(name)
if err != nil {
return "", err
}
return cookie.Value, nil
}
func (c *CookieHandler) GetEncryptedCookieValue(r *http.Request, name string, value interface{}) error {
cookie, err := r.Cookie(name)
if err != nil {
return err
}
if c.securecookie == nil {
return errors.ThrowInternal(nil, "HTTP-X6XpnL", "securecookie not configured")
}
if err := c.securecookie.Decode(name, cookie.Value, value); err != nil {
return err
}
return nil
}
func (c *CookieHandler) SetCookie(w http.ResponseWriter, name string, value string) {
c.httpSet(w, name, value, c.maxAge)
}
func (c *CookieHandler) SetEncryptedCookie(w http.ResponseWriter, name string, value interface{}) error {
if c.securecookie == nil {
return errors.ThrowInternal(nil, "HTTP-s2HUtx", "securecookie not configured")
}
encoded, err := c.securecookie.Encode(name, value)
if err != nil {
return err
}
c.httpSet(w, name, encoded, c.maxAge)
return nil
}
func (c *CookieHandler) DeleteCookie(w http.ResponseWriter, name string) {
c.httpSet(w, name, "", -1)
}
func (c *CookieHandler) httpSet(w http.ResponseWriter, name, value string, maxage int) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: value,
Domain: c.domain,
Path: c.path,
MaxAge: maxage,
HttpOnly: true,
Secure: c.secureOnly,
SameSite: c.sameSite,
})
}

View File

@@ -0,0 +1,28 @@
package http
import (
"net/http"
"github.com/gorilla/schema"
"github.com/caos/zitadel/internal/errors"
)
type Parser struct {
decoder *schema.Decoder
}
func NewParser() *Parser {
d := schema.NewDecoder()
d.IgnoreUnknownKeys(true)
return &Parser{d}
}
func (p *Parser) Parse(r *http.Request, data interface{}) error {
err := r.ParseForm()
if err != nil {
return errors.ThrowInternal(err, "FORM-lCC9zI", "error parsing http form")
}
return p.decoder.Decode(data, r.Form)
}