mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:57:32 +00:00
feat: port reduction (#323)
* move mgmt pkg * begin package restructure * rename auth package to authz * begin start api * move auth * move admin * fix merge * configs and interceptors * interceptor * revert generate-grpc.sh * some cleanups * console * move console * fix tests and merging * js linting * merge * merging and configs * change k8s base to current ports * fixes * cleanup * regenerate proto * remove unnecessary whitespace * missing param * go mod tidy * fix merging * move login pkg * cleanup * move api pkgs again * fix pkg naming * fix generate-static.sh for login * update workflow * fixes * logging * remove duplicate * comment for optional gateway interfaces * regenerate protos * fix proto imports for grpc web * protos * grpc web generate * grpc web generate * fix changes * add translation interceptor * fix merging * regenerate mgmt proto
This commit is contained in:
110
internal/api/authz/authorization.go
Normal file
110
internal/api/authz/authorization.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package authz
|
||||
|
||||
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, method string) (context.Context, error) {
|
||||
ctx, err := VerifyTokenAndWriteCtxData(ctx, token, orgID, verifier, method)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var perms []string
|
||||
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
|
||||
}
|
278
internal/api/authz/authorization_test.go
Normal file
278
internal/api/authz/authorization_test.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package authz
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
26
internal/api/authz/config.go
Normal file
26
internal/api/authz/config.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package authz
|
||||
|
||||
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
|
||||
}
|
57
internal/api/authz/context.go
Normal file
57
internal/api/authz/context.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/logging"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
const (
|
||||
permissionsKey key = 1
|
||||
dataKey key = 2
|
||||
)
|
||||
|
||||
type CtxData struct {
|
||||
UserID string
|
||||
OrgID string
|
||||
ProjectID string
|
||||
AgentID string
|
||||
PreferredLanguage string
|
||||
}
|
||||
|
||||
func (ctxData CtxData) IsZero() bool {
|
||||
return ctxData.UserID == "" || ctxData.OrgID == ""
|
||||
}
|
||||
|
||||
type Grants []*Grant
|
||||
|
||||
type Grant struct {
|
||||
OrgID string
|
||||
Roles []string
|
||||
}
|
||||
|
||||
func VerifyTokenAndWriteCtxData(ctx context.Context, token, orgID string, t *TokenVerifier, method string) (_ context.Context, err error) {
|
||||
userID, clientID, agentID, err := verifyAccessToken(ctx, token, t, method)
|
||||
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, dataKey, CtxData{UserID: userID, OrgID: orgID, ProjectID: projectID, AgentID: agentID}), nil
|
||||
}
|
||||
|
||||
func SetCtxData(ctx context.Context, ctxData CtxData) context.Context {
|
||||
return context.WithValue(ctx, dataKey, ctxData)
|
||||
}
|
||||
|
||||
func GetCtxData(ctx context.Context) CtxData {
|
||||
ctxData, _ := ctx.Value(dataKey).(CtxData)
|
||||
return ctxData
|
||||
}
|
||||
|
||||
func GetPermissionsFromCtx(ctx context.Context) []string {
|
||||
ctxPermission, _ := ctx.Value(permissionsKey).([]string)
|
||||
return ctxPermission
|
||||
}
|
7
internal/api/authz/context_mock.go
Normal file
7
internal/api/authz/context_mock.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package authz
|
||||
|
||||
import "context"
|
||||
|
||||
func NewMockContext(orgID, userID string) context.Context {
|
||||
return context.WithValue(nil, dataKey, CtxData{UserID: userID, OrgID: orgID})
|
||||
}
|
63
internal/api/authz/permissions.go
Normal file
63
internal/api/authz/permissions.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package authz
|
||||
|
||||
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")
|
||||
}
|
||||
grant, err := t.ResolveGrant(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if grant == nil {
|
||||
return context.WithValue(ctx, permissionsKey, []string{}), []string{}, nil
|
||||
}
|
||||
permissions := mapGrantToPermissions(requiredPerm, grant, authConfig)
|
||||
return context.WithValue(ctx, permissionsKey, permissions), permissions, nil
|
||||
}
|
||||
|
||||
func mapGrantToPermissions(requiredPerm string, grant *Grant, authConfig Config) []string {
|
||||
resolvedPermissions := make([]string, 0)
|
||||
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
|
||||
}
|
430
internal/api/authz/permissions_test.go
Normal file
430
internal/api/authz/permissions_test.go
Normal file
@@ -0,0 +1,430 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
func getTestCtx(userID, orgID string) context.Context {
|
||||
return context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID})
|
||||
}
|
||||
|
||||
type testVerifier struct {
|
||||
grant *Grant
|
||||
}
|
||||
|
||||
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID string) (string, string, error) {
|
||||
return "userID", "agentID", nil
|
||||
}
|
||||
|
||||
func (v *testVerifier) ResolveGrants(ctx context.Context) (*Grant, error) {
|
||||
return v.grant, nil
|
||||
}
|
||||
|
||||
func (v *testVerifier) ProjectIDByClientID(ctx context.Context, clientID string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (string, error) {
|
||||
return "clientID", 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: Start(&testVerifier{grant: &Grant{
|
||||
Roles: []string{"ORG_OWNER"},
|
||||
}}),
|
||||
requiredPerm: "project.read",
|
||||
authConfig: Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
{
|
||||
Role: "IAM_OWNER",
|
||||
Permissions: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
Role: "ORG_OWNER",
|
||||
Permissions: []string{"org.read", "project.read"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errFunc: caos_errs.IsUnauthenticated,
|
||||
result: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
name: "No Grants",
|
||||
args: args{
|
||||
ctx: getTestCtx("", ""),
|
||||
verifier: Start(&testVerifier{grant: &Grant{}}),
|
||||
requiredPerm: "project.read",
|
||||
authConfig: Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
{
|
||||
Role: "IAM_OWNER",
|
||||
Permissions: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
Role: "ORG_OWNER",
|
||||
Permissions: []string{"org.read", "project.read"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: make([]string, 0),
|
||||
},
|
||||
{
|
||||
name: "Get Permissions",
|
||||
args: args{
|
||||
ctx: getTestCtx("userID", "orgID"),
|
||||
verifier: Start(&testVerifier{grant: &Grant{
|
||||
Roles: []string{"ORG_OWNER"},
|
||||
}}),
|
||||
requiredPerm: "project.read",
|
||||
authConfig: Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
{
|
||||
Role: "IAM_OWNER",
|
||||
Permissions: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
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
|
||||
grant *Grant
|
||||
authConfig Config
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result []string
|
||||
}{
|
||||
{
|
||||
name: "One Role existing perm",
|
||||
args: args{
|
||||
requiredPerm: "project.read",
|
||||
grant: &Grant{Roles: []string{"ORG_OWNER"}},
|
||||
authConfig: Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
{
|
||||
Role: "IAM_OWNER",
|
||||
Permissions: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
Role: "ORG_OWNER",
|
||||
Permissions: []string{"org.read", "project.read"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
name: "One Role not existing perm",
|
||||
args: args{
|
||||
requiredPerm: "project.write",
|
||||
grant: &Grant{Roles: []string{"ORG_OWNER"}},
|
||||
authConfig: Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
{
|
||||
Role: "IAM_OWNER",
|
||||
Permissions: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
Role: "ORG_OWNER",
|
||||
Permissions: []string{"org.read", "project.read"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []string{},
|
||||
},
|
||||
{
|
||||
name: "Multiple Roles one existing",
|
||||
args: args{
|
||||
requiredPerm: "project.read",
|
||||
grant: &Grant{Roles: []string{"ORG_OWNER", "IAM_OWNER"}},
|
||||
authConfig: Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
{
|
||||
Role: "IAM_OWNER",
|
||||
Permissions: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
Role: "ORG_OWNER",
|
||||
Permissions: []string{"org.read", "project.read"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
name: "Multiple Roles, global and specific",
|
||||
args: args{
|
||||
requiredPerm: "project.read",
|
||||
grant: &Grant{Roles: []string{"ORG_OWNER", "PROJECT_OWNER:1"}},
|
||||
authConfig: Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
{
|
||||
Role: "PROJECT_OWNER",
|
||||
Permissions: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
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 := mapGrantToPermissions(tt.args.requiredPerm, tt.args.grant, 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{
|
||||
{
|
||||
Role: "IAM_OWNER",
|
||||
Permissions: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
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{
|
||||
{
|
||||
Role: "IAM_OWNER",
|
||||
Permissions: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
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{
|
||||
{
|
||||
Role: "PROJECT_OWNER",
|
||||
Permissions: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
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{
|
||||
{
|
||||
Role: "PROJECT_OWNER",
|
||||
Permissions: []string{"project.read"},
|
||||
},
|
||||
{
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
105
internal/api/authz/token.go
Normal file
105
internal/api/authz/token.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
BearerPrefix = "Bearer "
|
||||
)
|
||||
|
||||
type TokenVerifier struct {
|
||||
authZRepo authZRepo
|
||||
clients sync.Map
|
||||
authMethods MethodMapping
|
||||
}
|
||||
|
||||
type authZRepo interface {
|
||||
VerifyAccessToken(ctx context.Context, token, clientID string) (userID, agentID string, err error)
|
||||
VerifierClientID(ctx context.Context, name string) (clientID string, err error)
|
||||
ResolveGrants(ctx context.Context) (grant *Grant, err error)
|
||||
ProjectIDByClientID(ctx context.Context, clientID string) (projectID string, err error)
|
||||
}
|
||||
|
||||
func Start(authZRepo authZRepo) (v *TokenVerifier) {
|
||||
return &TokenVerifier{authZRepo: authZRepo}
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string, method string) (userID, clientID, agentID string, err error) {
|
||||
clientID, err = v.clientIDFromMethod(ctx, method)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
userID, agentID, err = v.authZRepo.VerifyAccessToken(ctx, token, clientID)
|
||||
return userID, clientID, agentID, err
|
||||
}
|
||||
|
||||
type client struct {
|
||||
id string
|
||||
name string
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) RegisterServer(appName, methodPrefix string, mappings MethodMapping) {
|
||||
v.clients.Store(methodPrefix, &client{name: appName})
|
||||
if v.authMethods == nil {
|
||||
v.authMethods = make(map[string]Option)
|
||||
}
|
||||
for method, option := range mappings {
|
||||
v.authMethods[method] = option
|
||||
}
|
||||
}
|
||||
|
||||
func prefixFromMethod(method string) (string, bool) {
|
||||
parts := strings.Split(method, "/")
|
||||
if len(parts) < 2 {
|
||||
return "", false
|
||||
}
|
||||
return parts[1], true
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) clientIDFromMethod(ctx context.Context, method string) (string, error) {
|
||||
prefix, ok := prefixFromMethod(method)
|
||||
if !ok {
|
||||
return "", caos_errs.ThrowPermissionDenied(nil, "AUTHZ-GRD2Q", "Errors.Internal")
|
||||
}
|
||||
app, ok := v.clients.Load(prefix)
|
||||
if !ok {
|
||||
return "", caos_errs.ThrowPermissionDenied(nil, "AUTHZ-G2qrh", "Errors.Internal")
|
||||
}
|
||||
var err error
|
||||
c := app.(*client)
|
||||
if c.id != "" {
|
||||
return c.id, nil
|
||||
}
|
||||
c.id, err = v.authZRepo.VerifierClientID(ctx, c.name)
|
||||
if err != nil {
|
||||
return "", caos_errs.ThrowPermissionDenied(err, "AUTHZ-ptTIF2", "Errors.Internal")
|
||||
}
|
||||
v.clients.Store(prefix, c)
|
||||
return c.id, nil
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) ResolveGrant(ctx context.Context) (*Grant, error) {
|
||||
return v.authZRepo.ResolveGrants(ctx)
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) {
|
||||
return v.authZRepo.ProjectIDByClientID(ctx, clientID)
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) CheckAuthMethod(method string) (Option, bool) {
|
||||
authOpt, ok := v.authMethods[method]
|
||||
return authOpt, ok
|
||||
}
|
||||
|
||||
func verifyAccessToken(ctx context.Context, token string, t *TokenVerifier, method string) (userID, clientID, agentID string, err error) {
|
||||
parts := strings.Split(token, BearerPrefix)
|
||||
if len(parts) != 2 {
|
||||
return "", "", "", caos_errs.ThrowUnauthenticated(nil, "AUTH-7fs1e", "invalid auth header")
|
||||
}
|
||||
return t.VerifyAccessToken(ctx, parts[1], method)
|
||||
}
|
75
internal/api/authz/token_test.go
Normal file
75
internal/api/authz/token_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
func Test_VerifyAccessToken(t *testing.T) {
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
token string
|
||||
verifier *TokenVerifier
|
||||
method string
|
||||
}
|
||||
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",
|
||||
verifier: &TokenVerifier{
|
||||
authZRepo: &testVerifier{grant: &Grant{}},
|
||||
clients: func() sync.Map {
|
||||
m := sync.Map{}
|
||||
m.Store("service", &client{name: "name"})
|
||||
return m
|
||||
}(),
|
||||
authMethods: MethodMapping{"/service/method": Option{Permission: "authenticated"}},
|
||||
},
|
||||
method: "/service/method",
|
||||
},
|
||||
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, tt.args.method)
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user