persist accounts using json, index using bleve
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
@@ -26,9 +26,10 @@ type LDAPSchema struct {
|
||||
|
||||
// Server configures a server.
|
||||
type Server struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Address string
|
||||
Name string
|
||||
Namespace string
|
||||
Address string
|
||||
AccountsDataPath string
|
||||
}
|
||||
|
||||
// Log defines the available logging configuration.
|
||||
|
||||
+6
-97
@@ -61,104 +61,13 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
|
||||
EnvVars: []string{"ACCOUNTS_ADDRESS"},
|
||||
Destination: &cfg.Server.Address,
|
||||
},
|
||||
// LDAP
|
||||
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-hostname",
|
||||
Value: "localhost",
|
||||
Usage: "LDAP hostname",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_HOSTNAME"},
|
||||
Destination: &cfg.LDAP.Hostname,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "ldap-port",
|
||||
Value: 9126,
|
||||
Usage: "LDAP port",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_PORT"},
|
||||
Destination: &cfg.LDAP.Port,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-base-dn",
|
||||
Value: "dc=example,dc=org",
|
||||
Usage: "LDAP basedn",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_BASE_DN"},
|
||||
Destination: &cfg.LDAP.BaseDN,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-userfilter",
|
||||
Value: "(&(objectclass=posixAccount)(cn=%s))",
|
||||
Usage: "LDAP userfilter",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_USERFILTER"},
|
||||
Destination: &cfg.LDAP.UserFilter,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-groupfilter",
|
||||
Value: "(&(objectclass=posixGroup)(cn=%s))",
|
||||
Usage: "LDAP groupfilter",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_GROUPFILTER"},
|
||||
Destination: &cfg.LDAP.GroupFilter,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-bind-dn",
|
||||
Value: "cn=reva,ou=sysusers,dc=example,dc=org",
|
||||
Usage: "LDAP bind dn",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_BIND_DN"},
|
||||
Destination: &cfg.LDAP.BindDN,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-bind-password",
|
||||
Value: "reva",
|
||||
Usage: "LDAP bind password",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_BIND_PASSWORD"},
|
||||
Destination: &cfg.LDAP.BindPassword,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-idp",
|
||||
Value: "https://localhost:9200",
|
||||
Usage: "Identity provider to use for users",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_IDP"},
|
||||
Destination: &cfg.LDAP.IDP,
|
||||
},
|
||||
// ldap dn is always the dn
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-schema-account-id",
|
||||
// TODO write down LDAP schema & register OID ownclouduuid
|
||||
//... use 'sourceAnchor','immutableid' see https://docs.microsoft.com/en-us/azure/active-directory/hybrid/plan-connect-design-concepts#sourceanchor
|
||||
// or 'ms-DS-ConsistencyGuid' see https://docs.microsoft.com/en-us/azure/active-directory/hybrid/plan-connect-design-concepts
|
||||
// or build a scim schema for ldap? https://ldapwiki.com/wiki/SCIM%20Common%20Attribute
|
||||
// glauth -> support id and externalid from scim
|
||||
Value: "uidNumber",
|
||||
Usage: "LDAP schema account id",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_SCHEMA_ACCOUNT_ID"},
|
||||
Destination: &cfg.LDAP.Schema.AccountID,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-schema-username",
|
||||
Value: "uid",
|
||||
Usage: "LDAP schema username",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_SCHEMA_USERNAME"},
|
||||
Destination: &cfg.LDAP.Schema.Username,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-schema-displayName",
|
||||
Value: "sn",
|
||||
Usage: "LDAP schema displayName",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_SCHEMA_DISPLAYNAME"},
|
||||
Destination: &cfg.LDAP.Schema.DisplayName,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-schema-mail",
|
||||
Value: "mail",
|
||||
Usage: "LDAP schema mail",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_SCHEMA_MAIL"},
|
||||
Destination: &cfg.LDAP.Schema.Mail,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-schema-cn",
|
||||
Value: "memberof",
|
||||
Usage: "LDAP schema cn",
|
||||
EnvVars: []string{"ACCOUNTS_LDAP_SCHEMA_GROUPS"},
|
||||
Destination: &cfg.LDAP.Schema.Groups,
|
||||
Name: "accounts-data-path",
|
||||
Value: "/var/tmp/ocis-accounts",
|
||||
DefaultText: "/var/tmp/ocis-accounts",
|
||||
Usage: "accounts folder",
|
||||
EnvVars: []string{"ACCOUNTS_DATA_PATH"},
|
||||
Destination: &cfg.Server.AccountsDataPath,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/CiscoM31/godata"
|
||||
"github.com/blevesearch/bleve"
|
||||
"github.com/blevesearch/bleve/search/query"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// add (ap)prox filter
|
||||
godata.GlobalFilterTokenizer = FilterTokenizer()
|
||||
godata.GlobalFilterParser.DefineOperator("ap", 2, godata.OpAssociationLeft, 4, false)
|
||||
}
|
||||
|
||||
// BuildBleveQuery converts a GoDataFilterQuery into a bleve query
|
||||
func BuildBleveQuery(r *godata.GoDataFilterQuery) (query.Query, error) {
|
||||
return recursiveBuildQuery(r.Tree)
|
||||
}
|
||||
|
||||
// Builds the filter recursively using DFS
|
||||
func recursiveBuildQuery(n *godata.ParseNode) (query.Query, error) {
|
||||
if n.Token.Type == godata.FilterTokenFunc {
|
||||
switch n.Token.Value {
|
||||
case "startswith":
|
||||
if len(n.Children) != 2 {
|
||||
return nil, errors.New("startswith match must have two children")
|
||||
}
|
||||
if n.Children[0].Token.Type != godata.FilterTokenLiteral {
|
||||
return nil, errors.New("startswith expected a literal as the first param")
|
||||
}
|
||||
if n.Children[1].Token.Type != godata.FilterTokenString {
|
||||
return nil, errors.New("startswith expected a string as the second param")
|
||||
}
|
||||
q := bleve.NewTermQuery(n.Children[1].Token.Value)
|
||||
q.SetField(n.Children[0].Token.Value)
|
||||
return q, nil
|
||||
default:
|
||||
return nil, godata.NotImplementedError(n.Token.Value + " is not implemented.")
|
||||
}
|
||||
}
|
||||
if n.Token.Type == godata.FilterTokenLogical {
|
||||
switch n.Token.Value {
|
||||
case "eq":
|
||||
if len(n.Children) != 2 {
|
||||
return nil, errors.New("Equality match must have two children")
|
||||
}
|
||||
if n.Children[0].Token.Type != godata.FilterTokenLiteral {
|
||||
return nil, errors.New("Equality expected a literal on the lhs")
|
||||
}
|
||||
if n.Children[1].Token.Type != godata.FilterTokenString {
|
||||
return nil, errors.New("Equality expected a string on the rhs")
|
||||
}
|
||||
q := bleve.NewTermQuery(n.Children[1].Token.Value)
|
||||
q.SetField(n.Children[0].Token.Value)
|
||||
return q, nil
|
||||
case "and":
|
||||
q := query.NewConjunctionQuery([]query.Query{})
|
||||
for _, child := range n.Children {
|
||||
subQuery, err := recursiveBuildQuery(child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if subQuery != nil {
|
||||
q.AddQuery(subQuery)
|
||||
}
|
||||
}
|
||||
return q, nil
|
||||
case "or":
|
||||
q := query.NewDisjunctionQuery([]query.Query{})
|
||||
for _, child := range n.Children {
|
||||
subQuery, err := recursiveBuildQuery(child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if subQuery != nil {
|
||||
q.AddQuery(subQuery)
|
||||
}
|
||||
}
|
||||
return q, nil
|
||||
case "Not":
|
||||
if len(n.Children) != 1 {
|
||||
return nil, errors.New("Not filter must have only one child")
|
||||
}
|
||||
subQuery, err := recursiveBuildQuery(n.Children[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := query.NewBooleanQuery(nil, nil, []query.Query{subQuery})
|
||||
return q, nil
|
||||
default:
|
||||
return nil, godata.NotImplementedError(n.Token.Value + " is not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
return nil, godata.NotImplementedError(n.Token.Value + " is not implemented.")
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/CiscoM31/godata"
|
||||
"github.com/owncloud/ocis-accounts/pkg/config"
|
||||
"gopkg.in/ldap.v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// add (ap)prox filter
|
||||
godata.GlobalFilterTokenizer = FilterTokenizer()
|
||||
godata.GlobalFilterParser.DefineOperator("ap", 2, godata.OpAssociationLeft, 4, false)
|
||||
}
|
||||
|
||||
// LDAPNodeMap is used to convert query tokens into ldap filters according to https://tools.ietf.org/search/rfc4515
|
||||
var LDAPNodeMap = map[string]string{
|
||||
// 11.2.6.1.1 Built-in Filter Operations according to http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#_Toc31358949
|
||||
|
||||
// Comparison Operators
|
||||
"eq": "(%s=%s)", // -> LDAP equal
|
||||
"ne": "(!(%s=%s))", // -> LDAP NOT equal
|
||||
//"gt": "(&(%s>=%s)(!(%s=%s)))", // -> TODO can be constructed but requires more parameters
|
||||
"ge": "(%s>=%s)", // -> LDAP greaterorequal
|
||||
//"lt": "(&(%s<=%s)(!(%s=%s)))", // -> TODO can be constructed but requires more parameters
|
||||
"le": "(%s<=%s)", // -> LDAP lessorequal
|
||||
//"has": "(%s=*)", // -> TODO LDAP present but in odata has looks like "Style has Sales.Color'Yellow'"
|
||||
//"in": "???", // TODO
|
||||
|
||||
// additional native LDAP Search String Filter Definition according to https://tools.ietf.org/search/rfc4515#section-3
|
||||
"ap": "(%s~=%s)", // approx, TODO needs token in parser, odata uses $search instead of $filter for fuzzy search
|
||||
|
||||
// Logical Operators
|
||||
// While LDAP understands logical filters like (&()()()()) we leave that as an optimization and use at max two params
|
||||
"and": "(&%s%s)",
|
||||
"or": "(|%s%s)",
|
||||
"not": "(!%s)",
|
||||
|
||||
// Arithmetic operators
|
||||
//"add": ""
|
||||
//"sub": ""
|
||||
//"mul": ""
|
||||
//"div": ""
|
||||
//"divby": ""
|
||||
//"mod": ""
|
||||
|
||||
// Grouping operators
|
||||
|
||||
// 11.2.6.1.2 Built-in Query Functions according to http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_BuiltinQueryFunctions
|
||||
//String and Collection Functions
|
||||
//"concat": "CONCAT(%s,%s)",
|
||||
"contains": "(%s=*%s*)",
|
||||
"endswith": "(%s=*%s)",
|
||||
//"indexof": "LOCATE(%s)",
|
||||
//"length": "LENGTH(%s)",
|
||||
"startswith": "(%s=%s*)",
|
||||
//"substring": "",
|
||||
|
||||
//
|
||||
//"tolower": "LOWER(%s)",
|
||||
//"toupper": "UPPER(%s)",
|
||||
//"trim": "TRIM(%s)",
|
||||
//"year": "YEAR(%s)",
|
||||
//"month": "MONTH(%s)",
|
||||
//"day": "DAY(%s)",
|
||||
//"hour": "HOUR(%s)",
|
||||
//"minute": "MINUTE(%s)",
|
||||
//"second": "SECOND(%s)",
|
||||
//"fractionalsecond": "MICROSECOND(%s)",
|
||||
//"date": "DATE(%s)",
|
||||
//"time": "TIME(%s)",
|
||||
//"totaloffsetminutes": "",
|
||||
//"now": "NOW()",
|
||||
//"maxdatetime":"",
|
||||
//"mindatetime":"",
|
||||
//"totalseconds":"",
|
||||
//"round": "ROUND(%s)",
|
||||
//"floor": "FLOOR(%s)",
|
||||
//"ceiling": "CEIL(%s)",
|
||||
//"isof": "", // TODO objectclass=
|
||||
//"cast": "",
|
||||
//"geo.distance": "",
|
||||
//"geo.intersects": "",
|
||||
//"geo.length": "",
|
||||
//"any": "",
|
||||
//"all": "",
|
||||
//"null": "NULL",
|
||||
}
|
||||
|
||||
// BuildLDAPFilter converts a GoDataFilterQuery into an ldap filter
|
||||
func BuildLDAPFilter(r *godata.GoDataFilterQuery, c *config.LDAPSchema) (string, error) {
|
||||
return recursiveBuildFilter(r.Tree, c)
|
||||
}
|
||||
|
||||
// Builds the filter recursively using DFS
|
||||
func recursiveBuildFilter(n *godata.ParseNode, c *config.LDAPSchema) (string, error) {
|
||||
if n.Token.Type == godata.FilterTokenLiteral {
|
||||
switch n.Token.Value {
|
||||
case "accountid":
|
||||
return c.AccountID, nil
|
||||
case "displayname":
|
||||
return c.DisplayName, nil
|
||||
case "username":
|
||||
return c.Username, nil
|
||||
case "mail":
|
||||
return c.Mail, nil
|
||||
case "groups":
|
||||
// TODO groups
|
||||
return "", godata.NotImplementedError(n.Token.Value + " is not implemented.")
|
||||
case "identities":
|
||||
// TODO identities
|
||||
return "", godata.NotImplementedError(n.Token.Value + " is not implemented.")
|
||||
}
|
||||
return "", godata.BadRequestError("unknown property " + n.Token.Value)
|
||||
}
|
||||
if n.Token.Type == godata.FilterTokenString {
|
||||
// without leading and ending ' required by odata
|
||||
// encode LDAP safe
|
||||
return strings.TrimSuffix(strings.TrimPrefix(ldap.EscapeFilter(n.Token.Value), "'"), "'"), nil
|
||||
}
|
||||
if n.Token.Type == godata.FilterTokenInteger {
|
||||
return n.Token.Value, nil
|
||||
}
|
||||
if n.Token.Type == godata.FilterTokenFloat {
|
||||
return n.Token.Value, nil
|
||||
}
|
||||
|
||||
if v, ok := LDAPNodeMap[n.Token.Value]; ok {
|
||||
children := []interface{}{}
|
||||
// build each child first using DFS
|
||||
for _, child := range n.Children {
|
||||
f, err := recursiveBuildFilter(child, c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
children = append(children, f)
|
||||
}
|
||||
// merge together the children and the current node
|
||||
result := fmt.Sprintf(v, children...)
|
||||
return result, nil
|
||||
}
|
||||
return "", godata.NotImplementedError(n.Token.Value + " is not implemented.")
|
||||
}
|
||||
|
||||
// FilterTokenizer creates a tokenizer capable of tokenizing filter statements
|
||||
// TODO disable tokens we don't handle anyway
|
||||
func FilterTokenizer() *godata.Tokenizer {
|
||||
t := godata.Tokenizer{}
|
||||
t.Add("^[0-9]{4,4}-[0-9]{2,2}-[0-9]{2,2}T[0-9]{2,2}:[0-9]{2,2}(:[0-9]{2,2}(.[0-9]+)?)?(Z|[+-][0-9]{2,2}:[0-9]{2,2})", godata.FilterTokenDateTime)
|
||||
t.Add("^-?[0-9]{4,4}-[0-9]{2,2}-[0-9]{2,2}", godata.FilterTokenDate)
|
||||
t.Add("^[0-9]{2,2}:[0-9]{2,2}(:[0-9]{2,2}(.[0-9]+)?)?", godata.FilterTokenTime)
|
||||
t.Add("^\\(", godata.FilterTokenOpenParen)
|
||||
t.Add("^\\)", godata.FilterTokenCloseParen)
|
||||
t.Add("^/", godata.FilterTokenNav)
|
||||
t.Add("^:", godata.FilterTokenColon)
|
||||
t.Add("^,", godata.FilterTokenComma)
|
||||
t.Add("^(geo.distance|geo.intersects|geo.length)", godata.FilterTokenFunc)
|
||||
t.Add("^(substringof|substring|length|indexof)", godata.FilterTokenFunc)
|
||||
// only change from the global tokenizer is the added ap
|
||||
t.Add("^(eq|ne|gt|ge|lt|le|and|or|not|has|in|ap)", godata.FilterTokenLogical)
|
||||
t.Add("^(add|sub|mul|divby|div|mod)", godata.FilterTokenOp)
|
||||
t.Add("^(contains|endswith|startswith|tolower|toupper|"+
|
||||
"trim|concat|year|month|day|hour|minute|second|fractionalseconds|date|"+
|
||||
"time|totaloffsetminutes|now|maxdatetime|mindatetime|totalseconds|round|"+
|
||||
"floor|ceiling|isof|cast)", godata.FilterTokenFunc)
|
||||
t.Add("^(any|all)", godata.FilterTokenLambda)
|
||||
t.Add("^null", godata.FilterTokenNull)
|
||||
t.Add("^\\$it", godata.FilterTokenIt)
|
||||
t.Add("^\\$root", godata.FilterTokenRoot)
|
||||
t.Add("^-?[0-9]+\\.[0-9]+", godata.FilterTokenFloat)
|
||||
t.Add("^-?[0-9]+", godata.FilterTokenInteger)
|
||||
t.Add("^'(''|[^'])*'", godata.FilterTokenString)
|
||||
t.Add("^(true|false)", godata.FilterTokenBoolean)
|
||||
t.Add("^@*[a-zA-Z][a-zA-Z0-9_.]*", godata.FilterTokenLiteral) // The optional '@' character is used to identify parameter aliases
|
||||
t.Ignore("^ ", godata.FilterTokenWhitespace)
|
||||
|
||||
return &t
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/CiscoM31/godata"
|
||||
"github.com/owncloud/ocis-accounts/pkg/config"
|
||||
)
|
||||
|
||||
var c *config.LDAPSchema
|
||||
|
||||
func init() {
|
||||
c = &config.LDAPSchema{
|
||||
AccountID: "ownclouduuid",
|
||||
Username: "uid",
|
||||
Mail: "mail",
|
||||
DisplayName: "displayname",
|
||||
}
|
||||
}
|
||||
|
||||
func TestEQ(t *testing.T) { testLDAPFilters(t, "accountid eq 'a-b-c-d'", "(ownclouduuid=a-b-c-d)") }
|
||||
func TestNE(t *testing.T) { testLDAPFilters(t, "mail ne 'foo@bar.com'", "(!(mail=foo@bar.com))") }
|
||||
func TestGE(t *testing.T) { testLDAPFilters(t, "displayname ge 'marie'", "(displayname>=marie)") }
|
||||
func TestLE(t *testing.T) { testLDAPFilters(t, "username le 'marie'", "(uid<=marie)") }
|
||||
|
||||
//func TestHas(t *testing.T) { testLDAPFilters(t, "Style has Sales.Color'Yellow'", "(foo=*)") }
|
||||
func TestAP(t *testing.T) {
|
||||
testLDAPFilters(t, "displayname ap 'einstein'", "(displayname~=einstein)")
|
||||
}
|
||||
func TestAND(t *testing.T) {
|
||||
testLDAPFilters(t, "accountid le 500000 and accountid ge 300000", "(&(ownclouduuid<=500000)(ownclouduuid>=300000))")
|
||||
}
|
||||
func TestOR(t *testing.T) {
|
||||
testLDAPFilters(t, "accountid le 700000 or accountid ge 900000", "(|(ownclouduuid<=700000)(ownclouduuid>=900000))")
|
||||
}
|
||||
func TestNOT(t *testing.T) {
|
||||
// not operator takes precedence over ap, so we need brackets
|
||||
testLDAPFilters(t, "not ( displayname ap 'einstein' )", "(!(displayname~=einstein))")
|
||||
}
|
||||
func TestContains(t *testing.T) {
|
||||
testLDAPFilters(t, "contains(username,'eins')", "(uid=*eins*)")
|
||||
}
|
||||
func TestStartsWith(t *testing.T) {
|
||||
testLDAPFilters(t, "startswith(username,'eins')", "(uid=eins*)")
|
||||
}
|
||||
func TestEndsWith(t *testing.T) {
|
||||
testLDAPFilters(t, "endswith(username,'eins')", "(uid=*eins)")
|
||||
}
|
||||
func TestEncoding(t *testing.T) {
|
||||
testLDAPFilters(t, "displayname eq 'eins(*)tein'", "(displayname=eins\\28\\2a\\29tein)")
|
||||
}
|
||||
|
||||
func testLDAPFilters(t *testing.T, have string, want string) {
|
||||
|
||||
var err error
|
||||
|
||||
var q *godata.GoDataFilterQuery
|
||||
if q, err = godata.ParseFilterString(have); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
var filter string
|
||||
if filter, err = BuildLDAPFilter(q, c); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if filter != want {
|
||||
t.Error("expected", want, "for", have, "but got", filter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package provider
|
||||
|
||||
import "github.com/CiscoM31/godata"
|
||||
|
||||
// FilterTokenizer creates a tokenizer capable of tokenizing filter statements
|
||||
// TODO disable tokens we don't handle anyway
|
||||
func FilterTokenizer() *godata.Tokenizer {
|
||||
t := godata.Tokenizer{}
|
||||
t.Add("^[0-9]{4,4}-[0-9]{2,2}-[0-9]{2,2}T[0-9]{2,2}:[0-9]{2,2}(:[0-9]{2,2}(.[0-9]+)?)?(Z|[+-][0-9]{2,2}:[0-9]{2,2})", godata.FilterTokenDateTime)
|
||||
t.Add("^-?[0-9]{4,4}-[0-9]{2,2}-[0-9]{2,2}", godata.FilterTokenDate)
|
||||
t.Add("^[0-9]{2,2}:[0-9]{2,2}(:[0-9]{2,2}(.[0-9]+)?)?", godata.FilterTokenTime)
|
||||
t.Add("^\\(", godata.FilterTokenOpenParen)
|
||||
t.Add("^\\)", godata.FilterTokenCloseParen)
|
||||
t.Add("^/", godata.FilterTokenNav)
|
||||
t.Add("^:", godata.FilterTokenColon)
|
||||
t.Add("^,", godata.FilterTokenComma)
|
||||
t.Add("^(geo.distance|geo.intersects|geo.length)", godata.FilterTokenFunc)
|
||||
t.Add("^(substringof|substring|length|indexof)", godata.FilterTokenFunc)
|
||||
// only change from the global tokenizer is the added ap
|
||||
t.Add("^(eq|ne|gt|ge|lt|le|and|or|not|has|in|ap)", godata.FilterTokenLogical)
|
||||
t.Add("^(add|sub|mul|divby|div|mod)", godata.FilterTokenOp)
|
||||
t.Add("^(contains|endswith|startswith|tolower|toupper|"+
|
||||
"trim|concat|year|month|day|hour|minute|second|fractionalseconds|date|"+
|
||||
"time|totaloffsetminutes|now|maxdatetime|mindatetime|totalseconds|round|"+
|
||||
"floor|ceiling|isof|cast)", godata.FilterTokenFunc)
|
||||
t.Add("^(any|all)", godata.FilterTokenLambda)
|
||||
t.Add("^null", godata.FilterTokenNull)
|
||||
t.Add("^\\$it", godata.FilterTokenIt)
|
||||
t.Add("^\\$root", godata.FilterTokenRoot)
|
||||
t.Add("^-?[0-9]+\\.[0-9]+", godata.FilterTokenFloat)
|
||||
t.Add("^-?[0-9]+", godata.FilterTokenInteger)
|
||||
t.Add("^'(''|[^'])*'", godata.FilterTokenString)
|
||||
t.Add("^(true|false)", godata.FilterTokenBoolean)
|
||||
t.Add("^@*[a-zA-Z][a-zA-Z0-9_.]*", godata.FilterTokenLiteral) // The optional '@' character is used to identify parameter aliases
|
||||
t.Ignore("^ ", godata.FilterTokenWhitespace)
|
||||
|
||||
return &t
|
||||
}
|
||||
+79
-149
@@ -2,12 +2,16 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/CiscoM31/godata"
|
||||
"github.com/blevesearch/bleve"
|
||||
"github.com/blevesearch/bleve/search/query"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
mclient "github.com/micro/go-micro/v2/client"
|
||||
"github.com/owncloud/ocis-accounts/pkg/config"
|
||||
@@ -16,13 +20,55 @@ import (
|
||||
olog "github.com/owncloud/ocis-pkg/v2/log"
|
||||
settings "github.com/owncloud/ocis-settings/pkg/proto/v0"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/ldap.v2"
|
||||
)
|
||||
|
||||
// New returns a new instance of Service
|
||||
func New(cfg *config.Config) Service {
|
||||
// read all user and group records
|
||||
|
||||
// for now recreate index on every start
|
||||
os.RemoveAll(filepath.Join(cfg.Server.AccountsDataPath, "index.bleve"))
|
||||
os.MkdirAll(filepath.Join(cfg.Server.AccountsDataPath, "accounts"), 0700)
|
||||
|
||||
mapping := bleve.NewIndexMapping()
|
||||
// TODO don't bother to store fields as we will load the account from disk
|
||||
index, err := bleve.New(filepath.Join(cfg.Server.AccountsDataPath, "index.bleve"), mapping)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f, err := os.Open(filepath.Join(cfg.Server.AccountsDataPath, "accounts"))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("dir", filepath.Join(cfg.Server.AccountsDataPath, "accounts")).Msg("could not open acconts folder")
|
||||
panic(err)
|
||||
}
|
||||
list, err := f.Readdir(-1)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("dir", filepath.Join(cfg.Server.AccountsDataPath, "accounts")).Msg("could not list accounts folder")
|
||||
panic(err)
|
||||
}
|
||||
for _, file := range list {
|
||||
path := filepath.Join(cfg.Server.AccountsDataPath, "accounts", file.Name())
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("could not read account")
|
||||
continue
|
||||
}
|
||||
a := proto.Account{}
|
||||
err = json.Unmarshal(data, &a)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("could not unmarshal account")
|
||||
continue
|
||||
}
|
||||
log.Debug().Interface("account", a).Msg("found account")
|
||||
index.Index(a.Id, a)
|
||||
}
|
||||
|
||||
// TODO watch folders for new records
|
||||
|
||||
s := Service{
|
||||
Config: cfg,
|
||||
index: index,
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -31,53 +77,7 @@ func New(cfg *config.Config) Service {
|
||||
// Service implements the AccountsServiceHandler interface
|
||||
type Service struct {
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
func (s Service) getBoundConnection(binddn string, password string) (l *ldap.Conn, err error) {
|
||||
l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", s.Config.LDAP.Hostname, s.Config.LDAP.Port), &tls.Config{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = l.Bind(binddn, password)
|
||||
if err != nil {
|
||||
l.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s Service) lookupDN(username string) (binddn string, err error) {
|
||||
l, err := s.getBoundConnection(s.Config.LDAP.BindDN, s.Config.LDAP.BindPassword)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
filter := fmt.Sprintf("(%s=%s)", s.Config.LDAP.Schema.Username, ldap.EscapeFilter(username))
|
||||
|
||||
// Search for the given username
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
s.Config.LDAP.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
[]string{"dn"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := l.Search(searchRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch len(sr.Entries) {
|
||||
case 0: // TODO return not found error
|
||||
case 1:
|
||||
return sr.Entries[0].DN, nil
|
||||
default: // TODO return too many results error?
|
||||
}
|
||||
return "", fmt.Errorf("dn not found for %s", filter)
|
||||
index bleve.Index
|
||||
}
|
||||
|
||||
// the auth request is currently hardcoded and has to macth this regex
|
||||
@@ -89,82 +89,50 @@ var authQuery = regexp.MustCompile(`^username eq '(.*)' and password eq '(.*)'$`
|
||||
// TODO id vs onpremiseimmutableid
|
||||
func (s Service) ListAccounts(ctx context.Context, in *proto.ListAccountsRequest, res *proto.ListAccountsResponse) (err error) {
|
||||
|
||||
var binddn string
|
||||
var password string
|
||||
|
||||
// check if this looks like an auth request
|
||||
match := authQuery.FindStringSubmatch(in.Query)
|
||||
if len(match) == 3 {
|
||||
|
||||
binddn, err = s.lookupDN(match[1])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("ListAccounts with auth request")
|
||||
return
|
||||
}
|
||||
log.Debug().Str("username", match[1]).Str("binddn", binddn).Msg("ListAccounts with auth request")
|
||||
|
||||
password = match[2]
|
||||
// remove password from query
|
||||
in.Query = fmt.Sprintf("username eq '%s'", match[1])
|
||||
} else {
|
||||
log.Debug().Str("query", in.Query).Int32("page-size", in.PageSize).Str("page-token", in.PageToken).Msg("ListAccounts")
|
||||
binddn = s.Config.LDAP.BindDN
|
||||
password = s.Config.LDAP.BindPassword
|
||||
}
|
||||
|
||||
filter := "(&)" // see Absolute True and False Filters in https://tools.ietf.org/html/rfc4526#section-2
|
||||
var query query.Query
|
||||
|
||||
if in.Query != "" {
|
||||
// parse the query like an odata filter
|
||||
var q *godata.GoDataFilterQuery
|
||||
if q, err = godata.ParseFilterString(in.Query); err != nil {
|
||||
log.Error().Err(err).Msg("could not parse query")
|
||||
return
|
||||
}
|
||||
|
||||
// convert to ldap filter
|
||||
filter, err = provider.BuildLDAPFilter(q, &s.Config.LDAP.Schema)
|
||||
// convert to bleve query
|
||||
query, err = provider.BuildBleveQuery(q)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("could not build bleve query")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
query = bleve.NewMatchAllQuery()
|
||||
}
|
||||
|
||||
log.Debug().Str("filter", filter).Msg("using filter")
|
||||
log.Debug().Interface("query", query).Msg("using query")
|
||||
|
||||
var l *ldap.Conn
|
||||
l, err = s.getBoundConnection(binddn, password)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
// TODO combine the parsed query with a query filter from the config, eg. fmt.Sprintf(s.Config.LDAP.UserFilter, clientID)
|
||||
|
||||
// Search for the given clientID
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
s.Config.LDAP.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
[]string{"dn", s.Config.LDAP.Schema.AccountID, s.Config.LDAP.Schema.Username, s.Config.LDAP.Schema.DisplayName, s.Config.LDAP.Schema.Mail, s.Config.LDAP.Schema.Groups}, // TODO Groups, Identities?
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := l.Search(searchRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug().Interface("entries", sr.Entries).Msg("entries")
|
||||
searchRequest := bleve.NewSearchRequest(query)
|
||||
searchResult, err := s.index.Search(searchRequest)
|
||||
log.Debug().Interface("result", searchResult).Msg("result")
|
||||
|
||||
res.Accounts = make([]*proto.Account, 0)
|
||||
for i := range sr.Entries {
|
||||
res.Accounts = append(res.Accounts, &proto.Account{
|
||||
Id: sr.Entries[i].GetAttributeValue(s.Config.LDAP.Schema.AccountID),
|
||||
// TODO identities
|
||||
Username: sr.Entries[i].GetAttributeValue(s.Config.LDAP.Schema.Username),
|
||||
DisplayName: sr.Entries[i].GetAttributeValue(s.Config.LDAP.Schema.DisplayName),
|
||||
Mail: sr.Entries[i].GetAttributeValue(s.Config.LDAP.Schema.Mail),
|
||||
//Groups: sr.Entries[i].GetAttributeValues(s.Config.LDAP.Schema.Groups),
|
||||
})
|
||||
|
||||
for _, hit := range searchResult.Hits {
|
||||
path := filepath.Join(s.Config.Server.AccountsDataPath, "accounts", hit.ID)
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("could not read account")
|
||||
continue
|
||||
}
|
||||
a := proto.Account{}
|
||||
err = json.Unmarshal(data, &a)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("could not unmarshal account")
|
||||
continue
|
||||
}
|
||||
log.Debug().Interface("account", a).Msg("found account")
|
||||
res.Accounts = append(res.Accounts, &a)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -172,45 +140,7 @@ func (s Service) ListAccounts(ctx context.Context, in *proto.ListAccountsRequest
|
||||
|
||||
// GetAccount implements the AccountsServiceHandler interface
|
||||
func (s Service) GetAccount(c context.Context, req *proto.GetAccountRequest, res *proto.Account) (err error) {
|
||||
|
||||
l, err := s.getBoundConnection(s.Config.LDAP.BindDN, s.Config.LDAP.BindPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
// TODO combine the parsed query with a query filter from the config, eg. fmt.Sprintf(s.Config.LDAP.UserFilter, clientID)
|
||||
filter := fmt.Sprintf("(%s=%s)", s.Config.LDAP.Schema.AccountID, ldap.EscapeFilter(req.Id))
|
||||
|
||||
// Search for the given clientID
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
s.Config.LDAP.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
[]string{"dn", s.Config.LDAP.Schema.AccountID, s.Config.LDAP.Schema.Username, s.Config.LDAP.Schema.DisplayName, s.Config.LDAP.Schema.Mail, s.Config.LDAP.Schema.Groups}, // TODO Groups, Identities?
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := l.Search(searchRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug().Interface("entries", sr.Entries).Msg("entries")
|
||||
|
||||
switch len(sr.Entries) {
|
||||
case 0: // TODO return not found error
|
||||
case 1:
|
||||
res.Id = sr.Entries[0].GetAttributeValue(s.Config.LDAP.Schema.AccountID)
|
||||
// TODO identities?
|
||||
res.Username = sr.Entries[0].GetAttributeValue(s.Config.LDAP.Schema.Username)
|
||||
res.DisplayName = sr.Entries[0].GetAttributeValue(s.Config.LDAP.Schema.DisplayName)
|
||||
res.Mail = sr.Entries[0].GetAttributeValue(s.Config.LDAP.Schema.Mail)
|
||||
// TODO groups
|
||||
default: // TODO return too many results error?
|
||||
}
|
||||
|
||||
return nil
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
// CreateAccount implements the AccountsServiceHandler interface
|
||||
|
||||
Reference in New Issue
Block a user