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..cf63410f0c2efb4c3d2150ce3956a75c5e1cbfab --- /dev/null +++ b/pkg/auth/client.go @@ -0,0 +1,255 @@ +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" +) + +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 ErrAccessDenied + } + + if c.IsSystem(ctx) { + return nil + } + + if spaceID != "" { + if c.spaceID == "" { + return ErrAccessDenied + } + + client, _ := c.Client(ctx) + if client != nil && client.SpaceID == spaceID { + return nil + } + } + + if c.Member(ctx).IsPrivileged() { + return nil + } + + return 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, ErrNotFound) { + if sp := c.getSpace(ctx, spaceID); sp == nil { + return false, 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..8522ec94ff4ac7e04e6eabfdca561f698645db9d --- /dev/null +++ b/pkg/auth/errors.go @@ -0,0 +1,10 @@ +package auth + +import ( + "git.perx.ru/perxis/perxis-go/pkg/errors" +) + +var ( + ErrAccessDenied = errors.PermissionDenied(errors.New("access denied")) + ErrNotFound = errors.NotFound(errors.New("not found")) +) 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..7a566711db76c11b22206d947ba94ecc2bc3b366 --- /dev/null +++ b/pkg/auth/grpc.go @@ -0,0 +1,92 @@ +package auth + +import ( + "context" + + kitgrpc "github.com/go-kit/kit/transport/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +const ( + 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) != "" { + (*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 + } +} + +// PrincipalServerInterceptor - grpc-интерÑептор, который иÑпользуетÑÑ Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ… принципала из grpc-метаданы и Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð² контекÑÑ‚ ''. Ð’ Ñлучае, еÑли +// ÑÐµÑ€Ð²Ð¸Ñ Ð½Ðµ иÑпользует проверку прав 'Principal' к ÑиÑтеме, в параметрах передаетÑÑ Ð¿ÑƒÑтой объект '&PrincipalFactory{}' +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) + } +} + +// PrincipalClientInterceptor - grpc-интерÑептор, который иÑпользуетÑÑ Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ… принципала. Ð’ Ñлучае, еÑли +// ÑÐµÑ€Ð²Ð¸Ñ Ð½Ðµ иÑпользует проверку прав 'Principal' к ÑиÑтеме, в параметрах передаетÑÑ Ð¿ÑƒÑтой объект '&PrincipalFactory{}' +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..f34693bf93f9b63c5d5eca84398b096af86e52ef --- /dev/null +++ b/pkg/auth/user.go @@ -0,0 +1,334 @@ +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" +) + +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 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 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, ErrNotFound) || errors.Is(rErr, ErrNotFound) { + if sp := u.getSpace(ctx, spaceID); sp == nil { + return false, 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, ErrNotFound) || errors.Is(rErr, ErrNotFound) { + if sp := u.getSpace(ctx, spaceID); sp == nil { + return false, 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) +} diff --git a/pkg/roles/role.go b/pkg/roles/role.go index 4c284ee5d45026aedfc284ecc16e146e78b56bb2..eecafb64135a8b4545fe7ef894529641ddbab69a 100644 --- a/pkg/roles/role.go +++ b/pkg/roles/role.go @@ -39,25 +39,33 @@ func (r Role) CanAccessEnvironment(ctx context.Context, service environments.Env return false } + if r.AllowManagement { + return true + } + // ЕÑли Ñвно не указаны доÑтупные Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ - доÑтуп по умолчанию к окружению master if len(r.Environments) == 0 { r.Environments = []string{environments.DefaultEnvironment} } - if data.Contains(envID, r.Environments) { - return true + for _, e := range r.Environments { + if envID == e || data.GlobMatch(envID, e) { + return true + } } - e, err := service.Get(ctx, spaceID, envID) - if err != nil || e == nil { + env, err := service.Get(ctx, spaceID, envID) + if err != nil || env == nil { return false } - aliases := append(e.Aliases, e.ID) + aliases := append(env.Aliases, env.ID) - for _, ce := range r.Environments { - if data.Contains(ce, aliases) { - return true + for _, e := range r.Environments { + for _, a := range aliases { + if a == e || data.GlobMatch(a, e) { + return true + } } }