diff --git a/pkg/auth/anonymous.go b/pkg/auth/anonymous.go
new file mode 100644
index 0000000000000000000000000000000000000000..62aa8a1ac28c45f09a065906a7735e44c1add05e
--- /dev/null
+++ b/pkg/auth/anonymous.go
@@ -0,0 +1,127 @@
+package auth
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"git.perx.ru/perxis/perxis-go/pkg/permission"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+)
+
+type Anonymous struct {
+	roles        roles.Roles
+	spaces       spaces.Spaces
+	spaceID      string
+	environments environments.Environments
+}
+
+func (Anonymous) GetID(ctx context.Context) string  { return "anonymous" }
+func (Anonymous) IsValid(ctx context.Context) bool  { return false }
+func (Anonymous) IsSystem(ctx context.Context) bool { return false }
+func (Anonymous) IsManagementAllowed(ctx context.Context, spaceID string) error {
+	return ErrAccessDenied
+}
+
+func (a Anonymous) Space(spaceID string) SpaceAccessor {
+	a.spaceID = spaceID
+	return &a
+}
+
+func (a *Anonymous) getSpace(ctx context.Context, spaceID string) *spaces.Space {
+	if spaceID == "" {
+		return nil
+	}
+	space, _ := a.spaces.Get(WithSystem(ctx), spaceID)
+	return space
+}
+
+func (a *Anonymous) HasSpaceAccess(ctx context.Context, spaceID string) bool {
+	if a.spaceID == "" || a.spaces == nil {
+		return false
+	}
+	return a.Role(ctx, spaceID) != nil
+}
+
+func (a *Anonymous) Member(ctx context.Context) members.Role {
+	return members.NotMember
+}
+
+func (a *Anonymous) Role(ctx context.Context, spaceID string) *roles.Role {
+	if a.spaceID == "" || a.roles == nil {
+		return nil
+	}
+	role, err := a.roles.Get(WithSystem(ctx), spaceID, roles.AnonymousRole)
+	if err != nil {
+		return nil
+	}
+	return role
+}
+
+func (a *Anonymous) Rules(ctx context.Context, spaceID, envID string) permission.Ruleset {
+	role := a.Role(WithSystem(ctx), spaceID)
+	if role == nil {
+		return nil
+	}
+
+	if !a.HasEnvironmentAccess(ctx, spaceID, envID) {
+		return nil
+	}
+
+	return role.Rules
+}
+
+func (a *Anonymous) HasEnvironmentAccess(ctx context.Context, space, env string) bool {
+	return hasEnvironmentAccess(ctx, a.environments, a.Role(ctx, space), env)
+}
+
+func (Anonymous) Format(f fmt.State, verb rune) {
+	f.Write([]byte("AnonymousPrincipal{}"))
+}
+
+func (a Anonymous) HasAccess(ctx context.Context, spaceID, orgID string) error {
+	if !a.IsValid(ctx) {
+		return ErrAccessDenied
+	}
+
+	if a.IsSystem(ctx) {
+		return nil
+	}
+
+	if spaceID != "" {
+		hasAllow, err := a.hasRole(ctx, spaceID)
+		if err != nil {
+			return err
+		}
+
+		if hasAllow {
+			return nil
+		}
+	}
+
+	if a.Member(ctx).IsPrivileged() {
+		return nil
+	}
+
+	return ErrAccessDenied
+}
+
+func (a *Anonymous) hasRole(ctx context.Context, spaceID string) (bool, error) {
+	if a.spaceID == "" || a.roles == nil {
+		return false, nil
+	}
+	_, err := a.roles.Get(WithSystem(ctx), spaceID, roles.AnonymousRole)
+	if err == nil {
+		return true, nil
+	}
+
+	if errors.Is(err, ErrNotFound) {
+		if sp := a.getSpace(ctx, spaceID); sp == nil {
+			return false, ErrNotFound
+		}
+	}
+	return false, nil
+}
diff --git a/pkg/auth/client.go b/pkg/auth/client.go
new file mode 100644
index 0000000000000000000000000000000000000000..6e0f07f0dee963ac43b9cae10b67f0282382bfb7
--- /dev/null
+++ b/pkg/auth/client.go
@@ -0,0 +1,256 @@
+package auth
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"git.perx.ru/perxis/perxis-go/pkg/permission"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+	"git.perx.ru/perxis/perxis/services"
+)
+
+type ClientPrincipal struct {
+	identity *clients.GetByParams
+	spaceID  string
+	space    *spaces.Space
+
+	client  *clients.Client
+	invalid bool
+
+	spaces        spaces.Spaces
+	environments  environments.Environments
+	clients       clients.Clients
+	roles         roles.Roles
+	collaborators collaborators.Collaborators
+}
+
+func NewClientPrincipal(identity *clients.GetByParams) *ClientPrincipal {
+	return &ClientPrincipal{identity: identity}
+}
+
+func (c ClientPrincipal) Format(f fmt.State, verb rune) {
+	var identity string
+	switch {
+	case c.identity == nil:
+		identity = "<nil>"
+	case c.identity.APIKey != "":
+		identity = fmt.Sprintf("APIKey: '%s'", c.identity.APIKey)
+	case c.identity.OAuthClientID != "":
+		identity = fmt.Sprintf("OAuthClientID: '%s'", c.identity.OAuthClientID)
+	case c.identity.TLSSubject != "":
+		identity = fmt.Sprintf("TLSSubject: '%s'", c.identity.TLSSubject)
+	}
+
+	var id string
+	if c.client != nil {
+		id = c.client.ID
+	}
+
+	f.Write([]byte(fmt.Sprintf("ClientPrincipal{ID: '%s', Identity: {%s}}", id, identity)))
+}
+
+func (c *ClientPrincipal) Space(spaceID string) SpaceAccessor {
+	c.spaceID = spaceID
+	c.space = nil
+	c.invalid = false
+	c.client = nil
+	return c
+}
+
+func (c *ClientPrincipal) getSpace(ctx context.Context, spaceID string) *spaces.Space {
+	if spaceID == "" {
+		return nil
+	}
+	space, _ := c.spaces.Get(WithSystem(ctx), spaceID)
+	return space
+}
+
+func (ClientPrincipal) IsSystem(ctx context.Context) bool {
+	return false
+}
+
+func (c *ClientPrincipal) IsManagementAllowed(ctx context.Context, spaceID string) error {
+	if !c.IsValid(ctx) {
+		return ErrAccessDenied
+	}
+
+	if role := c.Role(ctx, spaceID); role != nil && role.AllowManagement {
+		return nil
+	}
+
+	return ErrAccessDenied
+}
+
+func (c *ClientPrincipal) Member(ctx context.Context) members.Role {
+	return members.NotMember
+}
+
+func (c *ClientPrincipal) HasSpaceAccess(ctx context.Context, spaceID string) bool {
+	if c.spaceID == "" {
+		return false
+	}
+	client, _ := c.Client(ctx)
+	return client != nil && client.SpaceID == spaceID
+}
+
+func (c *ClientPrincipal) GetID(ctx context.Context) string {
+	client, _ := c.Client(ctx)
+	if client == nil {
+		return ""
+	}
+	return client.ID
+}
+
+func (c *ClientPrincipal) GetIdentity(ctx context.Context) *clients.GetByParams {
+	return c.identity
+}
+
+func (c *ClientPrincipal) IsValid(ctx context.Context) bool {
+	if c == nil {
+		return false
+	}
+	client, _ := c.Client(ctx)
+	return client != nil
+}
+
+func (c *ClientPrincipal) Client(ctx context.Context) (*clients.Client, error) {
+	if c.invalid {
+		return nil, nil
+	}
+
+	if c.client != nil {
+		return c.client, nil
+	}
+
+	if c.clients == nil {
+		c.invalid = true
+		return nil, nil
+	}
+
+	client, err := c.clients.GetBy(WithSystem(ctx), c.spaceID, c.identity)
+	if err != nil || client == nil || client.IsDisabled() {
+		c.invalid = true
+		return nil, err
+	}
+
+	c.client = client
+	return c.client, nil
+}
+
+func (c *ClientPrincipal) HasEnvironmentAccess(ctx context.Context, spaceID, envID string) bool {
+	return hasEnvironmentAccess(ctx, c.environments, c.Role(ctx, spaceID), envID)
+}
+
+func (c *ClientPrincipal) getRoleID(ctx context.Context, spaceID string) (string, bool) {
+
+	if c.spaceID == "" || spaceID == "" {
+		return "", false
+	}
+
+	if spaceID == c.spaceID {
+		cl, _ := c.Client(ctx)
+		if cl == nil || cl.RoleID == "" {
+			return "", false
+		}
+
+		return cl.RoleID, true
+	}
+
+	rID, err := c.collaborators.Get(WithSystem(ctx), spaceID, c.spaceID)
+	if err != nil {
+		rID = roles.ViewRole
+	}
+	return rID, true
+
+}
+
+func (c *ClientPrincipal) Role(ctx context.Context, spaceID string) *roles.Role {
+	if c.spaceID == "" {
+		return nil
+	}
+
+	rID, ok := c.getRoleID(ctx, spaceID)
+	if !ok {
+		return nil
+	}
+
+	role, err := c.roles.Get(WithSystem(ctx), spaceID, rID)
+	if err == nil {
+		//c.hasRole = true
+		//c.role = role
+		return role
+	}
+
+	return nil
+}
+
+func (c *ClientPrincipal) Rules(ctx context.Context, spaceID, envID string) permission.Ruleset {
+	if c.spaceID == "" || spaceID == "" || envID == "" {
+		return nil
+	}
+
+	role := c.Role(ctx, spaceID)
+	if role == nil {
+		return nil
+	}
+
+	if role.AllowManagement {
+		return permission.PrivilegedRuleset{}
+	}
+
+	if hasEnvironmentAccess(ctx, c.environments, role, envID) {
+		return role.Rules
+	}
+	return nil
+}
+
+func (c *ClientPrincipal) HasAccess(ctx context.Context, spaceID, orgID string) error {
+	if !c.IsValid(ctx) {
+		return services.ErrAccessDenied
+	}
+
+	if c.IsSystem(ctx) {
+		return nil
+	}
+
+	if spaceID != "" {
+		if c.spaceID == "" {
+			return services.ErrAccessDenied
+		}
+
+		client, _ := c.Client(ctx)
+		if client != nil && client.SpaceID == spaceID {
+			return nil
+		}
+	}
+
+	if c.Member(ctx).IsPrivileged() {
+		return nil
+	}
+
+	return services.ErrAccessDenied
+}
+
+func (c *ClientPrincipal) hasRole(ctx context.Context, spaceID string) (bool, error) {
+	if c.spaceID == "" {
+		return false, nil
+	}
+
+	client, err := c.Client(ctx)
+	if err != nil && errors.Is(err, services.ErrNotFound) {
+		if sp := c.getSpace(ctx, spaceID); sp == nil {
+			return false, services.ErrNotFound
+		}
+	}
+	if client != nil && client.SpaceID == spaceID {
+		return true, nil
+	}
+
+	return false, nil
+}
diff --git a/pkg/auth/context.go b/pkg/auth/context.go
new file mode 100644
index 0000000000000000000000000000000000000000..d447681068fefc974089f351d7654056314796b7
--- /dev/null
+++ b/pkg/auth/context.go
@@ -0,0 +1,27 @@
+package auth
+
+import (
+	"context"
+)
+
+type principalKey struct{}
+
+func GetPrincipal(ctx context.Context) Principal {
+	p, _ := ctx.Value(principalKey{}).(Principal)
+	if p == nil {
+		return Anonymous{}
+	}
+	return p
+}
+
+func WithPrincipal(ctx context.Context, p Principal) context.Context {
+	if ctx == nil {
+		ctx = context.Background()
+	}
+
+	return context.WithValue(ctx, principalKey{}, p)
+}
+
+func WithSystem(ctx context.Context) context.Context {
+	return WithPrincipal(ctx, &SystemPrincipal{})
+}
diff --git a/pkg/auth/errors.go b/pkg/auth/errors.go
new file mode 100644
index 0000000000000000000000000000000000000000..cc127435ee293843a7912cda0b85a599dcac14bc
--- /dev/null
+++ b/pkg/auth/errors.go
@@ -0,0 +1,7 @@
+package auth
+
+import service "git.perx.ru/perxis/perxis/services"
+
+var (
+	ErrAccessDenied = service.ErrAccessDenied
+)
diff --git a/pkg/auth/factory.go b/pkg/auth/factory.go
new file mode 100644
index 0000000000000000000000000000000000000000..2394c62a2f15ca7605959b3f5b31996a5c79a164
--- /dev/null
+++ b/pkg/auth/factory.go
@@ -0,0 +1,82 @@
+package auth
+
+import (
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+	"git.perx.ru/perxis/perxis-go/pkg/users"
+)
+
+type PrincipalFactory struct {
+	users.Users
+	members.Members
+	collaborators.Collaborators
+	roles.Roles
+	clients.Clients
+	spaces.Spaces
+	environments.Environments
+}
+
+func (f PrincipalFactory) User(identity string) Principal {
+	return &UserPrincipal{
+		identity:      identity,
+		users:         f.Users,
+		members:       f.Members,
+		roles:         f.Roles,
+		collaborators: f.Collaborators,
+		spaces:        f.Spaces,
+		environments:  f.Environments,
+	}
+}
+
+func (f PrincipalFactory) Client(param *clients.GetByParams) Principal {
+	return &ClientPrincipal{
+		identity: param,
+		//authID:       authID,
+		clients:       f.Clients,
+		environments:  f.Environments,
+		roles:         f.Roles,
+		spaces:        f.Spaces,
+		collaborators: f.Collaborators,
+	}
+}
+
+func (f PrincipalFactory) Anonymous() Principal {
+	return &Anonymous{
+		roles:  f.Roles,
+		spaces: f.Spaces,
+	}
+}
+
+func (f PrincipalFactory) System() Principal {
+	return &SystemPrincipal{}
+}
+
+func (f PrincipalFactory) Principal(principalId string) Principal {
+	switch {
+	case strings.Contains(principalId, "Subject="):
+		return f.Client(&clients.GetByParams{TLSSubject: getSubject(principalId)})
+	case strings.HasSuffix(principalId, "@clients"):
+		return f.Client(&clients.GetByParams{OAuthClientID: strings.TrimSuffix(principalId, "@clients")})
+	case strings.HasPrefix(principalId, "API-Key"):
+		return f.Client(&clients.GetByParams{APIKey: strings.TrimPrefix(principalId, "API-Key ")})
+	default:
+		return f.User(principalId)
+	}
+}
+
+func getSubject(header string) string {
+	var p string
+	for _, part := range strings.Split(header, ";") {
+		if strings.Contains(part, "Subject") {
+			p = strings.TrimSuffix(strings.TrimPrefix(part, "Subject=\""), "\"")
+			break
+		}
+	}
+	return p
+}
diff --git a/pkg/auth/grpc.go b/pkg/auth/grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..8cc0e209dfa60e0271287d7b160f6bb9095c83c5
--- /dev/null
+++ b/pkg/auth/grpc.go
@@ -0,0 +1,93 @@
+package auth
+
+import (
+	"context"
+
+	kitgrpc "github.com/go-kit/kit/transport/grpc"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/metadata"
+)
+
+const (
+
+	// todo: объединить OAuth2IdentityMetadata, TLSIdentityMetadata, AuthorizationMetadata
+	// в один заголовок ??
+
+	OAuth2IdentityMetadata = "x-perxis-identity"
+	TLSIdentityMetadata    = "x-forwarded-client-cert"
+	AccessMetadata         = "x-perxis-access"
+
+	AuthorizationMetadata = "authorization"
+)
+
+func GRPCToContext(factory *PrincipalFactory) kitgrpc.ServerRequestFunc {
+	return func(ctx context.Context, md metadata.MD) context.Context {
+		if identity := md.Get(TLSIdentityMetadata); len(identity) > 0 {
+			return WithPrincipal(ctx, factory.Principal(identity[0]))
+		}
+
+		if identity := md.Get(OAuth2IdentityMetadata); len(identity) > 0 {
+			return WithPrincipal(ctx, factory.Principal(identity[0]))
+		}
+
+		if identity := md.Get(AuthorizationMetadata); len(identity) > 0 {
+			return WithPrincipal(ctx, factory.Principal(identity[0]))
+		}
+
+		if access := md.Get(AccessMetadata); len(access) > 0 {
+			return WithPrincipal(ctx, factory.System())
+		}
+
+		return WithPrincipal(ctx, factory.Anonymous())
+	}
+}
+
+func ContextToGRPC() kitgrpc.ClientRequestFunc {
+	return func(ctx context.Context, md *metadata.MD) context.Context {
+		p := GetPrincipal(ctx)
+
+		switch p := p.(type) {
+		case *UserPrincipal:
+			if p.GetIdentity(ctx) != "" {
+				//ctx = metadata.AppendToOutgoingContext(ctx, OAuth2IdentityMetadata, p.GetIdentity(ctx))
+				(*md)[OAuth2IdentityMetadata] = []string{p.GetIdentity(ctx)}
+			}
+		case *ClientPrincipal:
+			if ident := p.GetIdentity(ctx); ident != nil {
+				switch {
+				case ident.OAuthClientID != "":
+					(*md)[OAuth2IdentityMetadata] = []string{ident.OAuthClientID + "@clients"}
+				case ident.TLSSubject != "":
+					(*md)[TLSIdentityMetadata] = []string{ident.TLSSubject}
+				case ident.APIKey != "":
+					(*md)[AuthorizationMetadata] = []string{"API-Key " + ident.APIKey}
+
+				}
+			}
+		case *SystemPrincipal:
+			(*md)[AccessMetadata] = []string{p.GetID(ctx)}
+		}
+
+		return ctx
+	}
+}
+
+func PrincipalServerInterceptor(factory *PrincipalFactory) grpc.UnaryServerInterceptor {
+	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
+		if md, ok := metadata.FromIncomingContext(ctx); ok {
+			ctx = GRPCToContext(factory)(ctx, md)
+		}
+		return handler(ctx, req)
+	}
+}
+
+func PrincipalClientInterceptor() grpc.UnaryClientInterceptor {
+	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
+		md, ok := metadata.FromOutgoingContext(ctx)
+		if !ok {
+			md = metadata.MD{}
+		}
+		ctx = metadata.NewOutgoingContext(ContextToGRPC()(ctx, &md), md)
+		return invoker(ctx, method, req, reply, cc, opts...)
+	}
+}
diff --git a/pkg/auth/principal.go b/pkg/auth/principal.go
new file mode 100644
index 0000000000000000000000000000000000000000..f2a3948fc5fbf24038261165ac84c0ef08da9c04
--- /dev/null
+++ b/pkg/auth/principal.go
@@ -0,0 +1,86 @@
+package auth
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"git.perx.ru/perxis/perxis-go/pkg/permission"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"git.perx.ru/perxis/perxis/util"
+)
+
+type Principal interface {
+	GetID(ctx context.Context) string
+	IsValid(ctx context.Context) bool
+	IsSystem(ctx context.Context) bool
+	HasAccess(ctx context.Context, spID, orgID string) error
+
+	IsManagementAllowed(ctx context.Context, spaceID string) error
+}
+
+type SpaceAccessor interface {
+	Principal
+	Space(spaceID string) SpaceAccessor
+
+	// HasSpaceAccess проверяет, есть ли у принципала доступ на чтение пространства
+	// (просмотр информации о пространстве, окружений, т.д. - доступ к записям коллекций
+	// определяется отдельным набором правил, см. SpaceAccessor.Rules())
+	HasSpaceAccess(ctx context.Context, spaceID string) bool
+	HasEnvironmentAccess(ctx context.Context, spaceID, env string) bool
+
+	// Member возвращает роль принципала в организации
+	Member(ctx context.Context) members.Role
+
+	Role(ctx context.Context, spaceID string) *roles.Role
+
+	// Rules возвращает набор правил, по которым принципал может получить
+	// доступ к записям коллекций пространства.
+	Rules(ctx context.Context, spaceID, envID string) permission.Ruleset
+}
+
+type OrganizationAccessor interface {
+	Principal
+	Organization(orgID string) OrganizationAccessor
+	Member(ctx context.Context) members.Role
+}
+
+func hasEnvironmentAccess(ctx context.Context, envsrv environments.Environments, role *roles.Role, envID string) bool {
+	if role == nil || role.SpaceID == "" || envID == "" {
+		return false
+	}
+
+	if role.AllowManagement {
+		return true
+	}
+
+	envs := role.Environments
+
+	// Если явно не указаны доступные окружения - доступ по умолчанию к окружению master
+	if len(envs) == 0 {
+		envs = []string{environments.DefaultEnvironment}
+	}
+
+	for _, ce := range envs {
+		if envID == ce || util.GlobMatch(envID, ce) {
+			return true
+		}
+	}
+
+	e, err := envsrv.Get(WithSystem(ctx), role.SpaceID, envID)
+	if err != nil || e == nil {
+		return false
+	}
+
+	aliases := append(e.Aliases, e.ID)
+
+	for _, ce := range envs {
+		for _, al := range aliases {
+			if al == ce || util.GlobMatch(al, ce) {
+				return true
+			}
+		}
+	}
+
+	return false
+}
diff --git a/pkg/auth/principal_test.go b/pkg/auth/principal_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..54e04ee4a8bf96151f9dfa9cf1edff0342ab03dd
--- /dev/null
+++ b/pkg/auth/principal_test.go
@@ -0,0 +1,178 @@
+package auth
+
+import (
+	"context"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	mocksenvs "git.perx.ru/perxis/perxis-go/pkg/environments/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"github.com/stretchr/testify/mock"
+)
+
+func Test_hasEnvironmentAccess(t *testing.T) {
+	type args struct {
+		ctx      context.Context
+		envscall func(envsservice *mocksenvs.Environments)
+		role     *roles.Role
+		envID    string
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "simple",
+			args: args{
+				ctx: context.Background(),
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"env1", "env2"},
+				},
+				envID: "env1",
+			},
+			want: true,
+		},
+		{
+			name: "glob env in role test: e*",
+			args: args{
+				ctx: context.Background(),
+				envscall: func(envsservice *mocksenvs.Environments) {
+					envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{
+						ID:      "env1",
+						SpaceID: "space",
+						Aliases: []string{"master"},
+					}, nil).Once()
+				},
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"e*"},
+				},
+				envID: "env",
+			},
+			want: true,
+		},
+		{
+			name: "glob env in role test: *n*",
+			args: args{
+				ctx: context.Background(),
+				envscall: func(envsservice *mocksenvs.Environments) {
+					envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{
+						ID:      "env1",
+						SpaceID: "space",
+						Aliases: []string{"master"},
+					}, nil).Once()
+				},
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"*n*"},
+				},
+				envID: "env",
+			},
+			want: true,
+		},
+		{
+			name: "glob env in role test: *1",
+			args: args{
+				ctx: context.Background(),
+				envscall: func(envsservice *mocksenvs.Environments) {
+					envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{
+						ID:      "env1",
+						SpaceID: "space",
+						Aliases: []string{"master"},
+					}, nil).Once()
+				},
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"*1"},
+				},
+				envID: "env",
+			},
+			want: true,
+		},
+		{
+			name: "glob env in role test (alias): ma*",
+			args: args{
+				ctx: context.Background(),
+				envscall: func(envsservice *mocksenvs.Environments) {
+					envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{
+						ID:      "env1",
+						SpaceID: "space",
+						Aliases: []string{"master"},
+					}, nil).Once()
+				},
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"ma*"},
+				},
+				envID: "env1",
+			},
+			want: true,
+		},
+		{
+			name: "glob env in role test: *",
+			args: args{
+				ctx: context.Background(),
+				envscall: func(envsservice *mocksenvs.Environments) {
+					envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{
+						ID:      "env1",
+						SpaceID: "space",
+						Aliases: []string{"master"},
+					}, nil).Once()
+				},
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"*"},
+				},
+				envID: "env1",
+			},
+			want: true,
+		},
+		{
+			name: "glob env in role test: q*",
+			args: args{
+				ctx: context.Background(),
+				envscall: func(envsservice *mocksenvs.Environments) {
+					envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{
+						ID:      "env1",
+						SpaceID: "space",
+						Aliases: []string{"master"},
+					}, nil).Once()
+				},
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"q*"},
+				},
+				envID: "env1",
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			envsservice := &mocksenvs.Environments{}
+			if tt.args.envscall != nil {
+				tt.args.envscall(envsservice)
+			}
+
+			if got := hasEnvironmentAccess(tt.args.ctx, envsservice, tt.args.role, tt.args.envID); got != tt.want {
+				t.Errorf("hasEnvironmentAccess() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/auth/system.go b/pkg/auth/system.go
new file mode 100644
index 0000000000000000000000000000000000000000..b602fe114bb53d5f82ae5f6796806dc20ed75188
--- /dev/null
+++ b/pkg/auth/system.go
@@ -0,0 +1,39 @@
+package auth
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"git.perx.ru/perxis/perxis-go/pkg/permission"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+)
+
+type SystemPrincipal struct{}
+
+const (
+	SystemID = "system"
+)
+
+func (p SystemPrincipal) GetID(ctx context.Context) string                            { return SystemID }
+func (SystemPrincipal) IsValid(ctx context.Context) bool                              { return true }
+func (SystemPrincipal) IsSystem(ctx context.Context) bool                             { return true }
+func (SystemPrincipal) IsManagementAllowed(ctx context.Context, spaceID string) error { return nil }
+
+func (p SystemPrincipal) Organization(_ string) OrganizationAccessor { return p }
+
+func (p SystemPrincipal) Space(_ string) SpaceAccessor                  { return p }
+func (SystemPrincipal) HasSpaceAccess(_ context.Context, _ string) bool { return true }
+func (SystemPrincipal) HasAccess(ctx context.Context, spaceID, orgID string) error {
+	return nil
+}
+func (SystemPrincipal) HasEnvironmentAccess(_ context.Context, _, _ string) bool { return true }
+func (SystemPrincipal) Member(_ context.Context) members.Role                    { return members.NotMember }
+func (SystemPrincipal) Role(_ context.Context, _ string) *roles.Role             { return nil }
+func (SystemPrincipal) Rules(_ context.Context, _, _ string) permission.Ruleset {
+	return &permission.PrivilegedRuleset{}
+}
+
+func (SystemPrincipal) Format(f fmt.State, verb rune) {
+	f.Write([]byte("SystemPrincipal{}"))
+}
diff --git a/pkg/auth/user.go b/pkg/auth/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..2101848b81d15483801061ccf11345882e7f1f71
--- /dev/null
+++ b/pkg/auth/user.go
@@ -0,0 +1,335 @@
+package auth
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"git.perx.ru/perxis/perxis-go/pkg/permission"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+	"git.perx.ru/perxis/perxis-go/pkg/users"
+	"git.perx.ru/perxis/perxis/services"
+)
+
+type UserPrincipal struct {
+	id       string
+	identity string
+
+	user    *users.User
+	invalid bool
+	spaceID string
+	orgID   string
+
+	users         users.Users
+	members       members.Members
+	hasMemberRole bool
+	memberRole    members.Role
+
+	collaborators collaborators.Collaborators
+	spaces        spaces.Spaces
+	environments  environments.Environments
+	roles         roles.Roles
+}
+
+func (u UserPrincipal) Format(f fmt.State, verb rune) {
+	f.Write([]byte(fmt.Sprintf("UserPrincipal{ID: '%s', Identity: '%s'}", u.id, u.identity)))
+}
+
+func (u *UserPrincipal) Space(spaceID string) SpaceAccessor {
+	u.spaceID = spaceID
+	u.orgID = ""
+	return u
+}
+
+func (u *UserPrincipal) getSpace(ctx context.Context, spaceID string) *spaces.Space {
+	if spaceID == "" {
+		return nil
+	}
+	space, _ := u.spaces.Get(WithSystem(ctx), spaceID)
+	return space
+}
+
+func (u UserPrincipal) Organization(orgID string) OrganizationAccessor {
+	u.orgID = orgID
+	return &u
+}
+
+func (u *UserPrincipal) GetID(ctx context.Context) string {
+	user := u.User(ctx)
+	if user == nil {
+		return ""
+	}
+	return user.ID
+}
+
+func (u *UserPrincipal) GetIdentity(ctx context.Context) string {
+	return u.identity
+}
+
+func (u *UserPrincipal) IsValid(ctx context.Context) bool {
+	if u == nil {
+		return false
+	}
+
+	return u.User(ctx) != nil
+}
+
+func (u *UserPrincipal) IsSystem(ctx context.Context) bool {
+	user := u.User(ctx)
+	if user != nil {
+		return user.IsSystem()
+	}
+	return false
+}
+
+func (u *UserPrincipal) IsManagementAllowed(ctx context.Context, spaceID string) error {
+	if !u.IsValid(ctx) {
+		return ErrAccessDenied
+	}
+
+	if u.IsSystem(ctx) {
+		return nil
+	}
+
+	if u.Member(ctx).IsPrivileged() {
+		return nil
+	}
+
+	if role := u.Role(ctx, spaceID); role != nil && role.AllowManagement {
+		return nil
+	}
+
+	return ErrAccessDenied
+}
+
+func (u *UserPrincipal) User(ctx context.Context) *users.User {
+	if u.invalid {
+		return nil
+	}
+
+	if u.user != nil {
+		return u.user
+	}
+	if u.users == nil {
+		u.invalid = true
+		return nil
+	}
+
+	var user *users.User
+	var err error
+	switch {
+	case u.id != "":
+		user, err = u.users.Get(WithSystem(ctx), u.id)
+	case u.identity != "":
+		ctx = WithSystem(ctx)
+		user, err = u.users.GetByIdentity(WithSystem(ctx), u.identity)
+	}
+
+	if err != nil || user == nil {
+		u.invalid = true
+		return nil
+	}
+
+	u.user = user
+	return u.user
+}
+
+func (u *UserPrincipal) Member(ctx context.Context) members.Role {
+	if u.hasMemberRole {
+		return u.memberRole
+	}
+
+	if u.members == nil || (u.orgID == "" && u.spaceID == "") {
+		u.hasMemberRole = true
+		return members.NotMember
+	}
+
+	if u.orgID == "" && u.spaceID != "" {
+		sp := u.getSpace(ctx, u.spaceID)
+		if sp == nil {
+			u.hasMemberRole = true
+			return members.NotMember
+		}
+		u.orgID = sp.OrgID
+	}
+
+	role, err := u.members.Get(WithSystem(ctx), u.orgID, u.GetID(ctx))
+	if err != nil {
+		role = members.NotMember
+	}
+
+	u.memberRole = role
+	u.hasMemberRole = true
+	return u.memberRole
+}
+
+// HasSpaceAccess проверяет, есть ли у пользователя доступ к пространству
+// Пользователь имеет доступ к пространству если:
+// - Является участником пространства (даже если его роль не существует)
+// - Пространство позволяет доступ для не участников (есть роли AnonymousRole/AuthorizedRole/ViewRole)
+// Deprecated :use HasAccess
+func (u *UserPrincipal) HasSpaceAccess(ctx context.Context, spaceID string) bool {
+	res, _ := u.hasRole(ctx, spaceID)
+	return res
+}
+
+// HasAccess проверяет, есть ли у пользователя доступ к пространству
+// Пользователь имеет доступ к пространству если:
+// - Является участником пространства (даже если его роль не существует)
+// - Пространство позволяет доступ для не участников (есть роли AnonymousRole/AuthorizedRole/ViewRole)
+func (u *UserPrincipal) HasAccess(ctx context.Context, spaceID, orgID string) error {
+	if !u.IsValid(ctx) {
+		return services.ErrAccessDenied
+	}
+
+	if u.IsSystem(ctx) {
+		return nil
+	}
+
+	if spaceID != "" {
+		hasAllow, err := u.hasRole(ctx, spaceID)
+		if err != nil {
+			return err
+		}
+
+		if hasAllow {
+			return nil
+		}
+	}
+
+	if orgID != "" {
+		if u.Organization(orgID).Member(ctx).IsPrivileged() {
+			return nil
+		}
+	} else {
+		if u.Member(ctx).IsPrivileged() {
+			return nil
+		}
+	}
+
+	return services.ErrAccessDenied
+}
+
+func (u *UserPrincipal) hasRole(ctx context.Context, spaceID string) (bool, error) {
+
+	if u.spaceID == "" || spaceID == "" {
+		return false, nil
+	}
+
+	ctx = WithSystem(ctx)
+
+	if spaceID != u.spaceID {
+		_, cErr := u.collaborators.Get(ctx, spaceID, u.spaceID)
+		if cErr == nil {
+			return true, nil
+		}
+		_, rErr := u.roles.Get(ctx, spaceID, roles.ViewRole)
+		if rErr == nil {
+			return true, nil
+		}
+		if errors.Is(cErr, services.ErrNotFound) || errors.Is(rErr, services.ErrNotFound) {
+			if sp := u.getSpace(ctx, spaceID); sp == nil {
+				return false, services.ErrNotFound
+			}
+		}
+
+		return false, nil
+	}
+
+	_, cErr := u.collaborators.Get(ctx, spaceID, u.GetID(ctx))
+	if cErr == nil {
+		return true, nil
+	}
+
+	_, rErr := u.roles.Get(ctx, spaceID, roles.AuthorizedRole)
+	if rErr == nil {
+		return true, nil
+	}
+
+	if errors.Is(cErr, services.ErrNotFound) || errors.Is(rErr, services.ErrNotFound) {
+		if sp := u.getSpace(ctx, spaceID); sp == nil {
+			return false, services.ErrNotFound
+		}
+	}
+
+	return false, nil
+}
+
+func (u *UserPrincipal) getRoleID(ctx context.Context, spaceID string) string {
+
+	if u.spaceID == "" || spaceID == "" {
+		return ""
+	}
+
+	ctx = WithSystem(ctx)
+
+	if spaceID != u.spaceID {
+		rID, err := u.collaborators.Get(ctx, spaceID, u.spaceID)
+		if err != nil {
+			rID = roles.ViewRole
+		}
+		return rID
+	}
+
+	if roleID, err := u.collaborators.Get(ctx, spaceID, u.GetID(ctx)); err == nil {
+		return roleID
+	}
+
+	return roles.AuthorizedRole
+}
+
+func (u *UserPrincipal) Role(ctx context.Context, spaceID string) *roles.Role {
+
+	if roleID := u.getRoleID(ctx, spaceID); roleID != "" {
+		role, _ := u.roles.Get(WithSystem(ctx), spaceID, roleID)
+		return role
+	}
+
+	return nil
+}
+
+func (u *UserPrincipal) Rules(ctx context.Context, spaceID, envID string) permission.Ruleset {
+	if spaceID == "" || envID == "" {
+		return nil
+	}
+
+	if u.spaceID == spaceID && (u.IsSystem(ctx) || u.Member(ctx).IsPrivileged()) {
+		return permission.PrivilegedRuleset{}
+	}
+
+	role := u.Role(ctx, spaceID)
+	if role == nil {
+		return nil
+	}
+
+	if !hasEnvironmentAccess(ctx, u.environments, role, envID) {
+		return nil
+	}
+
+	return role.Rules
+}
+
+func IsValidUser(ctx context.Context, p Principal) bool {
+	if p == nil {
+		return false
+	}
+	if u, ok := p.(*UserPrincipal); ok {
+		return u.IsValid(ctx)
+	}
+	return false
+}
+
+func User(ctx context.Context, p Principal) *users.User {
+	if u, ok := p.(*UserPrincipal); ok {
+		return u.User(ctx)
+	}
+	return nil
+}
+
+func (u *UserPrincipal) HasEnvironmentAccess(ctx context.Context, spaceID, env string) bool {
+	return hasEnvironmentAccess(ctx, u.environments, u.Role(ctx, spaceID), env)
+}