mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-30 17:00:57 -06:00
minimal webfinger (#5373)
* initial webfinger stub Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * add webfinger to proxy, return current host Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * some cleanup Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * allow passing multiple rel params Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * introduce interfaces Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * parse oidc auth token Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * add templating, drop chain, use map of relation providers Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * fix ocis url yaml Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * fix typos Co-authored-by: Dominik Schmidt <dschmidt@owncloud.com> * switch to userinfo claims Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * readme cleanup Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * add TODO.md with ideas Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * replace subject on authenticated request responses Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * Apply suggestions from code review Co-authored-by: Martin <github@diemattels.at> * markdown lint Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * return a 401 when bearer token expired, some more docs Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * Apply suggestions from code review Co-authored-by: Martin <github@diemattels.at> * fix docs Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * clarify env var Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * extract handler func Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * use correct service in reflex.conf Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * test relations Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * Update services/webfinger/pkg/config/config.go --------- Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> Co-authored-by: Dominik Schmidt <dschmidt@owncloud.com> Co-authored-by: Martin <github@diemattels.at>
This commit is contained in:
committed by
GitHub
parent
139cf79f61
commit
2c98d3246c
18
docs/services/webfinger/_index.md
Normal file
18
docs/services/webfinger/_index.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Webfinger
|
||||
date: 2023-02-03T00:00:00+00:00
|
||||
weight: 20
|
||||
geekdocRepo: https://github.com/owncloud/ocis
|
||||
geekdocEditPath: edit/master/docs/services/webfinger
|
||||
geekdocFilePath: _index.md
|
||||
geekdocCollapseSection: true
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
This service provides endpoints a the /.well-known/webfinger implementation.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
{{< toc-tree >}}
|
||||
|
||||
24
docs/services/wellknown/_index.md
Normal file
24
docs/services/wellknown/_index.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Well-Known
|
||||
date: 2023-02-03T00:00:00+00:00
|
||||
weight: 20
|
||||
geekdocRepo: https://github.com/owncloud/ocis
|
||||
geekdocEditPath: edit/master/docs/services/well-known
|
||||
geekdocFilePath: _index.md
|
||||
geekdocCollapseSection: true
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
This service provides endpoints on the /.well-known API
|
||||
|
||||
## Table of Contents
|
||||
|
||||
{{< toc-tree >}}
|
||||
|
||||
|
||||
## Webfinger
|
||||
|
||||
## oCIS-configuration
|
||||
|
||||
## Libregraph?
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
users "github.com/owncloud/ocis/v2/services/users/pkg/config"
|
||||
web "github.com/owncloud/ocis/v2/services/web/pkg/config"
|
||||
webdav "github.com/owncloud/ocis/v2/services/webdav/pkg/config"
|
||||
webfinger "github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -106,5 +107,6 @@ type Config struct {
|
||||
Users *users.Config `yaml:"users"`
|
||||
Web *web.Config `yaml:"web"`
|
||||
WebDAV *webdav.Config `yaml:"webdav"`
|
||||
Webfinger *webfinger.Config `yaml:"webfinger"`
|
||||
Search *search.Config `yaml:"search"`
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
users "github.com/owncloud/ocis/v2/services/users/pkg/config/defaults"
|
||||
web "github.com/owncloud/ocis/v2/services/web/pkg/config/defaults"
|
||||
webdav "github.com/owncloud/ocis/v2/services/webdav/pkg/config/defaults"
|
||||
webfinger "github.com/owncloud/ocis/v2/services/webfinger/pkg/config/defaults"
|
||||
)
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
@@ -71,5 +72,6 @@ func DefaultConfig() *Config {
|
||||
Users: users.DefaultConfig(),
|
||||
Web: web.DefaultConfig(),
|
||||
WebDAV: webdav.DefaultConfig(),
|
||||
Webfinger: webfinger.DefaultConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
93
ocis-pkg/middleware/oidc.go
Normal file
93
ocis-pkg/middleware/oidc.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
gOidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// newOidcOptions initializes the available default options.
|
||||
func newOidcOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// OIDCProvider used to mock the oidc provider during tests
|
||||
type OIDCProvider interface {
|
||||
UserInfo(ctx context.Context, ts oauth2.TokenSource) (*gOidc.UserInfo, error)
|
||||
}
|
||||
|
||||
// OidcAuth provides a middleware to authenticate a bearer auth with an OpenID Connect identity provider
|
||||
// It will put all claims provided by the userinfo endpoint in the context
|
||||
func OidcAuth(opts ...Option) func(http.Handler) http.Handler {
|
||||
opt := newOidcOptions(opts...)
|
||||
|
||||
// TODO use a micro store cache option
|
||||
|
||||
providerFunc := func() (OIDCProvider, error) {
|
||||
// Initialize a provider by specifying the issuer URL.
|
||||
// it will fetch the keys from the issuer using the .well-known
|
||||
// endpoint
|
||||
return gOidc.NewProvider(
|
||||
context.WithValue(context.Background(), oauth2.HTTPClient, http.Client{}),
|
||||
opt.OidcIssuer,
|
||||
)
|
||||
}
|
||||
var provider OIDCProvider
|
||||
getProviderOnce := sync.Once{}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
switch {
|
||||
case strings.HasPrefix(authHeader, "Bearer "):
|
||||
getProviderOnce.Do(func() {
|
||||
var err error
|
||||
provider, err = providerFunc()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
oauth2Token := &oauth2.Token{
|
||||
AccessToken: strings.TrimPrefix(authHeader, "Bearer "),
|
||||
}
|
||||
|
||||
userInfo, err := provider.UserInfo(
|
||||
context.WithValue(ctx, oauth2.HTTPClient, http.Client{}),
|
||||
oauth2.StaticTokenSource(oauth2Token),
|
||||
)
|
||||
if err != nil {
|
||||
w.Header().Add("WWW-Authenticate", `Bearer`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
claims := map[string]interface{}{}
|
||||
err = userInfo.Claims(&claims)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
ctx = oidc.NewContext(ctx, claims)
|
||||
|
||||
default:
|
||||
// do nothing
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
40
ocis-pkg/middleware/options.go
Normal file
40
ocis-pkg/middleware/options.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
// Logger to use for logging, must be set
|
||||
Logger log.Logger
|
||||
// The OpenID Connect Issuer URL
|
||||
OidcIssuer string
|
||||
// GatewayAPIClient is a reva gateway client
|
||||
GatewayAPIClient gatewayv1beta1.GatewayAPIClient
|
||||
}
|
||||
|
||||
// WithLogger provides a function to set the openid connect issuer option.
|
||||
func WithOidcIssuer(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.OidcIssuer = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger provides a function to set the logger option.
|
||||
func WithLogger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithGatewayAPIClient provides a function to set the reva gateway client option.
|
||||
func WithGatewayAPIClient(val gatewayv1beta1.GatewayAPIClient) Option {
|
||||
return func(o *Options) {
|
||||
o.GatewayAPIClient = val
|
||||
}
|
||||
}
|
||||
31
ocis/pkg/command/webfinger.go
Normal file
31
ocis/pkg/command/webfinger.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/parser"
|
||||
"github.com/owncloud/ocis/v2/ocis/pkg/command/helper"
|
||||
"github.com/owncloud/ocis/v2/ocis/pkg/register"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/command"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// WebfingerCommand is the entrypoint for the webfinger command.
|
||||
func WebfingerCommand(cfg *config.Config) *cli.Command {
|
||||
|
||||
return &cli.Command{
|
||||
Name: cfg.Webfinger.Service.Name,
|
||||
Usage: helper.SubcommandDescription(cfg.Webfinger.Service.Name),
|
||||
Category: "services",
|
||||
Before: func(c *cli.Context) error {
|
||||
configlog.Error(parser.ParseConfig(cfg, true))
|
||||
cfg.Webfinger.Commons = cfg.Commons
|
||||
return nil
|
||||
},
|
||||
Subcommands: command.GetCommands(cfg.Webfinger),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
register.AddCommand(WebfingerCommand)
|
||||
}
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
users "github.com/owncloud/ocis/v2/services/users/pkg/command"
|
||||
web "github.com/owncloud/ocis/v2/services/web/pkg/command"
|
||||
webdav "github.com/owncloud/ocis/v2/services/webdav/pkg/command"
|
||||
webfinger "github.com/owncloud/ocis/v2/services/webfinger/pkg/command"
|
||||
"github.com/thejerf/suture/v4"
|
||||
)
|
||||
|
||||
@@ -112,6 +113,7 @@ func NewService(options ...Option) (*Service, error) {
|
||||
s.ServicesRegistry[opts.Config.Thumbnails.Service.Name] = thumbnails.NewSutureService
|
||||
s.ServicesRegistry[opts.Config.Web.Service.Name] = web.NewSutureService
|
||||
s.ServicesRegistry[opts.Config.WebDAV.Service.Name] = webdav.NewSutureService
|
||||
s.ServicesRegistry[opts.Config.Webfinger.Service.Name] = webfinger.NewSutureService
|
||||
s.ServicesRegistry[opts.Config.Frontend.Service.Name] = frontend.NewSutureService
|
||||
s.ServicesRegistry[opts.Config.OCDav.Service.Name] = ocdav.NewSutureService
|
||||
s.ServicesRegistry[opts.Config.Gateway.Service.Name] = gateway.NewSutureService
|
||||
|
||||
@@ -74,7 +74,12 @@ func DefaultPolicies() []config.Policy {
|
||||
Unprotected: true,
|
||||
},
|
||||
{
|
||||
Endpoint: "/.well-known/",
|
||||
Endpoint: "/.well-known/webfinger",
|
||||
Service: "com.owncloud.web.webfinger",
|
||||
Unprotected: true,
|
||||
},
|
||||
{
|
||||
Endpoint: "/.well-known/openid-configuration",
|
||||
Service: "com.owncloud.web.idp",
|
||||
Unprotected: true,
|
||||
},
|
||||
|
||||
2
services/webfinger/.dockerignore
Normal file
2
services/webfinger/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!bin/
|
||||
39
services/webfinger/Makefile
Normal file
39
services/webfinger/Makefile
Normal file
@@ -0,0 +1,39 @@
|
||||
SHELL := bash
|
||||
NAME := webfinger
|
||||
|
||||
include ../../.make/recursion.mk
|
||||
|
||||
############ tooling ############
|
||||
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
|
||||
include ../../.bingo/Variables.mk
|
||||
endif
|
||||
|
||||
############ go tooling ############
|
||||
include ../../.make/go.mk
|
||||
|
||||
############ release ############
|
||||
include ../../.make/release.mk
|
||||
|
||||
############ docs generate ############
|
||||
include ../../.make/docs.mk
|
||||
|
||||
.PHONY: docs-generate
|
||||
docs-generate: config-docs-generate
|
||||
|
||||
############ generate ############
|
||||
include ../../.make/generate.mk
|
||||
|
||||
.PHONY: ci-go-generate
|
||||
ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this target
|
||||
$(MOCKERY) --srcpkg github.com/go-ldap/ldap/v3 --case underscore --filename ldapclient.go --name Client
|
||||
|
||||
|
||||
.PHONY: ci-node-generate
|
||||
ci-node-generate:
|
||||
|
||||
############ licenses ############
|
||||
.PHONY: ci-node-check-licenses
|
||||
ci-node-check-licenses:
|
||||
|
||||
.PHONY: ci-node-save-licenses
|
||||
ci-node-save-licenses:
|
||||
122
services/webfinger/README.md
Normal file
122
services/webfinger/README.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Webfinger Service
|
||||
|
||||
The webfinger service provides an RFC7033 WebFinger lookup of ownCloud instances relevant for a given user account.
|
||||
|
||||
It is based on https://github.com/owncloud/lookup-webfinger-sciebo but also returns localized `titles` in addition to the `href` property.
|
||||
|
||||
## OpenID Connect Discovery
|
||||
|
||||
Clients can make an unauthenticated `GET https://drive.ocis.test/.well-known/webfinger?resource=https%3A%2F%2Fcloud.ocis.test` request to discover the OpenID Connect Issuer in the `http://openid.net/specs/connect/1.0/issuer` relation:
|
||||
```json
|
||||
{
|
||||
"subject": "acct:einstein@drive.ocis.test",
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://openid.net/specs/connect/1.0/issuer",
|
||||
"href": "https://sso.example.org/cas/oidc/"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Here, the `resource` takes the instance domain URI, but an `acct:` URI works as well.
|
||||
|
||||
## Authenticated Instance Discovery
|
||||
|
||||
When using OpenID connect to authenticate requests, clients can look up the owncloud instances a user has access to.
|
||||
* Authentication is necessary to prevent leaking information about existing users.
|
||||
* Basic auth is not supported.
|
||||
|
||||
The default configuration will simply return the `OCIS_URL` and direct clients to that domain:
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": "acct:einstein@drive.ocis.test",
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://openid.net/specs/connect/1.0/issuer",
|
||||
"href": "https://sso.example.org/cas/oidc/"
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.owncloud/rel/server-instance",
|
||||
"href": "https://abc.drive.example.org",
|
||||
"titles": {
|
||||
"en": "oCIS Instance"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Configure Different Instances Based on OpenidConnect UserInfo Claims
|
||||
|
||||
A more complex example for configuring different instances could look like this:
|
||||
```yaml
|
||||
webfinger:
|
||||
instances:
|
||||
- claim: email
|
||||
regex: einstein@example\.org
|
||||
href: "https://{{.preferred_username}}.cloud.ocis.test"
|
||||
title:
|
||||
"en": "oCIS Instance for Einstein"
|
||||
"de": "oCIS Instanz für Einstein"
|
||||
break: true
|
||||
- claim: "email"
|
||||
regex: marie@example\.org
|
||||
href: "https://{{.preferred_username}}.cloud.ocis.test"
|
||||
title:
|
||||
"en": "oCIS Instance for Marie"
|
||||
"de": "oCIS Instanz für Marie"
|
||||
break: false
|
||||
- claim: "email"
|
||||
regex: .+@example\.org
|
||||
href: "https://example-org.cloud.ocis.test"
|
||||
title:
|
||||
"en": "oCIS Instance for example.org"
|
||||
"de": "oCIS Instanz für example.org"
|
||||
break: true
|
||||
- claim: "email"
|
||||
regex: .+@example\.com
|
||||
href: "https://example-com.cloud.ocis.test"
|
||||
title:
|
||||
"en": "oCIS Instance for example.com"
|
||||
"de": "oCIS Instanz für example.com"
|
||||
break: true
|
||||
- claim: "email"
|
||||
regex: .+@.+\..+
|
||||
href: "https://cloud.ocis.test"
|
||||
title:
|
||||
"en": "oCIS Instance"
|
||||
"de": "oCIS Instanz"
|
||||
break: true
|
||||
```
|
||||
|
||||
Now, an authenticated webfinger request for `acct:me@example.org` (when logged in as marie) would return two instances, based on her `email` claim, the regex matches and break flags:
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": "acct:marie@example.org",
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://openid.net/specs/connect/1.0/issuer",
|
||||
"href": "https://sso.example.org/cas/oidc/"
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.owncloud/rel/server-instance",
|
||||
"href": "https://marie.cloud.ocis.test",
|
||||
"titles": {
|
||||
"en": "oCIS Instance for Marie",
|
||||
"de": "oCIS Instanz für Marie"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.owncloud/rel/server-instance",
|
||||
"href": "https://xyz.drive.example.org",
|
||||
"titles": {
|
||||
"en": "oCIS Instance for example.org",
|
||||
"de": "oCIS Instanz für example.org"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
137
services/webfinger/TODO.md
Normal file
137
services/webfinger/TODO.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# TODO
|
||||
Currently, clients need to make subsequent calls to:
|
||||
* /status.php to check if the instance is in maintenance mode or if the version is supported
|
||||
* /config.json to get the available apps for ocis web to determine which routes require authentication
|
||||
* /themes/owncloud/theme.json for theming info
|
||||
* /.well-known/openid-configuration, auth2 token and oidc userinfo endpoints to authenticate the user
|
||||
* /ocs/v1.php/cloud/user to get the username, eg. einstein ... again? it contains the oc10 user id (marie, not the uuid)
|
||||
* /ocs/v1.php/cloud/capabilities to fetch instance capabilites
|
||||
* /ocs/v1.php/cloud/users/einstein to fetch the quota which could come from graph and actually is now tied to the spaces, not to users
|
||||
* /graph/v1.0/me?%24expand=memberOf to fetch the user id and the groups the user is a member of
|
||||
|
||||
We need a way to pass oidc claims from the proxy, which does the authentication to the webfinger service, preferably by minting them into the internal reva token.
|
||||
* Currently, we use machine auth so we can autoprovision an account if it does not exist. We should use revas oidc auth and, when autoprovisioning is enabled, retry the authentication after provisioning the account. This would allow us to use a `roles` claim to decide which roles to use and eg. a `school` claim to determine a specific instance. We may use https://github.com/PerimeterX/marshmallow to parse the RegisteredClaims and get the custom claims as a separate map.
|
||||
|
||||
For now, webfinger can only match users based on a regex and produce a list of instances based on that.
|
||||
|
||||
Here are some Ideas which need to be discussed with all client teams in the future:
|
||||
|
||||
## Implement a Backend Lookup
|
||||
|
||||
We could use ldap, the graph service or a reva based authentication to look up more properties that can be used to determine which instances to list. The initial implementation works on oidc claims and does not work with basic auth.
|
||||
|
||||
## Replace status.php with Properties
|
||||
|
||||
The /.well-known/webfinger enpdoint allows us to not only get rid of some of these calls, e.g. by embedding status.php info:
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": "https://drive.ocis.test",
|
||||
"properties": {
|
||||
"http://webfinger.owncloud/prop/maintenance": "false",
|
||||
"http://webfinger.owncloud/prop/version": "10.11.0.6"
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://openid.net/specs/connect/1.0/issuer",
|
||||
"href": "https://idp.ocis.test"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Introduce Dedicated ocis web Endpoint
|
||||
|
||||
It also allows us to move some services out of a sharded deployment. We could e.g. introduce a relation for a common ocis web endpoint to not exponse the different instances in the browser bar:
|
||||
```json
|
||||
{
|
||||
"subject": "acct:einstein@drive.ocis.test",
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://openid.net/specs/connect/1.0/issuer",
|
||||
"href": "https://idp.ocis.test"
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.owncloud/rel/web",
|
||||
"href": "https://drive.ocis.test"
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.owncloud/rel/server-instance",
|
||||
"href": "https://abc.drive.ocis.test",
|
||||
"titles": {
|
||||
"en": "Readable Instance Name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.owncloud/rel/server-instance",
|
||||
"href": "https://xyz.drive.ocis.test",
|
||||
"titles": {
|
||||
"en": "Readable Other Instance Name"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Dedicated ocis web Endpoint
|
||||
|
||||
We could also omit the `http://webfinger.owncloud/rel/server-instance` relation and go straight for a graph service with e.g. `rel=http://libregraph.org/rel/graph`:
|
||||
```json
|
||||
{
|
||||
"subject": "acct:einstein@drive.ocis.test",
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://openid.net/specs/connect/1.0/issuer",
|
||||
"href": "https://idp.ocis.test"
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.owncloud/rel/web",
|
||||
"href": "https://drive.ocis.test"
|
||||
},
|
||||
{
|
||||
"rel": "http://libregraph.org/rel/graph",
|
||||
"href": "https://abc.drive.ocis.test/graph/v1.0",
|
||||
"titles": {
|
||||
"en": "Readable Instance Name"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In theory the graph endpoint would allow discovering drives on any domain. But there is a lot more work to be done here.
|
||||
|
||||
## Subject Properties
|
||||
|
||||
We could also embed subject metadata, however since apps like ocis web also need the groups a user is member of a dedicated call to the libregraph api is probably better. In any case, we could return properties for the subject:
|
||||
```json
|
||||
{
|
||||
"subject": "acct:einstein@drive.ocis.test",
|
||||
"properties": {
|
||||
"http://libregraph.org/prop/user/id": "4c510ada-c86b-4815-8820-42cdf82c3d51",
|
||||
"http://libregraph.org/prop/user/onPremisesSamAccountName": "einstein",
|
||||
"http://libregraph.org/prop/user/mail": "einstein@example.org",
|
||||
"http://libregraph.org/prop/user/displayName": "Albert Einstein",
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://openid.net/specs/connect/1.0/issuer",
|
||||
"href": "https://idp.ocis.test"
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.owncloud/rel/server-instance",
|
||||
"href": "https://abc.drive.ocis.test",
|
||||
"titles": {
|
||||
"en": "Readable Instance Name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.owncloud/rel/server-instance",
|
||||
"href": "https://xyz.drive.ocis.test",
|
||||
"titles": {
|
||||
"en": "Readable Other Instance Name"
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
14
services/webfinger/cmd/webfinger/main.go
Normal file
14
services/webfinger/cmd/webfinger/main.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/command"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config/defaults"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := command.Execute(defaults.DefaultConfig()); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
54
services/webfinger/pkg/command/health.go
Normal file
54
services/webfinger/pkg/command/health.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/logging"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Health is the entrypoint for the health command.
|
||||
func Health(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "health",
|
||||
Usage: "check health status",
|
||||
Category: "info",
|
||||
Before: func(c *cli.Context) error {
|
||||
return configlog.ReturnError(parser.ParseConfig(cfg))
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
|
||||
resp, err := http.Get(
|
||||
fmt.Sprintf(
|
||||
"http://%s/healthz",
|
||||
cfg.Debug.Addr,
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed to request health check")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Fatal().
|
||||
Int("code", resp.StatusCode).
|
||||
Msg("Health seems to be in bad state")
|
||||
}
|
||||
|
||||
logger.Debug().
|
||||
Int("code", resp.StatusCode).
|
||||
Msg("Health got a good state")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
59
services/webfinger/pkg/command/root.go
Normal file
59
services/webfinger/pkg/command/root.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/clihelper"
|
||||
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// GetCommands provides all commands for this service
|
||||
func GetCommands(cfg *config.Config) cli.Commands {
|
||||
return []*cli.Command{
|
||||
// start this service
|
||||
Server(cfg),
|
||||
|
||||
// interaction with this service
|
||||
|
||||
// infos about this service
|
||||
Health(cfg),
|
||||
Version(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute is the entry point for the ocis webfinger command.
|
||||
func Execute(cfg *config.Config) error {
|
||||
app := clihelper.DefaultApp(&cli.App{
|
||||
Name: "webfinger",
|
||||
Usage: "Serve webfinger API for oCIS",
|
||||
Commands: GetCommands(cfg),
|
||||
})
|
||||
|
||||
return app.Run(os.Args)
|
||||
}
|
||||
|
||||
// SutureService allows for the webdav command to be embedded and supervised by a suture supervisor tree.
|
||||
type SutureService struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewSutureService creates a new webdav.SutureService
|
||||
func NewSutureService(cfg *ociscfg.Config) suture.Service {
|
||||
cfg.Webfinger.Commons = cfg.Commons
|
||||
return SutureService{
|
||||
cfg: cfg.Webfinger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s SutureService) Serve(ctx context.Context) error {
|
||||
s.cfg.Context = ctx
|
||||
if err := Execute(s.cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
142
services/webfinger/pkg/command/server.go
Normal file
142
services/webfinger/pkg/command/server.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/oklog/run"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/logging"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/metrics"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/relations"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/server/debug"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/server/http"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/tracing"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Server is the entrypoint for the server command.
|
||||
func Server(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "server",
|
||||
Usage: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
|
||||
Category: "server",
|
||||
Before: func(c *cli.Context) error {
|
||||
return configlog.ReturnFatal(parser.ParseConfig(cfg))
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
err := tracing.Configure(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
gr = run.Group{}
|
||||
ctx, cancel = func() (context.Context, context.CancelFunc) {
|
||||
if cfg.Context == nil {
|
||||
return context.WithCancel(context.Background())
|
||||
}
|
||||
return context.WithCancel(cfg.Context)
|
||||
}()
|
||||
metrics = metrics.New(metrics.Logger(logger))
|
||||
)
|
||||
|
||||
defer cancel()
|
||||
|
||||
metrics.BuildInfo.WithLabelValues(version.GetString()).Set(1)
|
||||
|
||||
{
|
||||
relationProviders, err := getRelationProviders(cfg)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("relation providier init")
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := service.New(
|
||||
service.Logger(logger),
|
||||
service.Config(cfg),
|
||||
service.WithRelationProviders(relationProviders),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("handler init")
|
||||
return err
|
||||
}
|
||||
svc = service.NewInstrument(svc, metrics)
|
||||
svc = service.NewLogging(svc, logger) // this logs service specific data
|
||||
svc = service.NewTracing(svc)
|
||||
|
||||
server, err := http.Server(
|
||||
http.Logger(logger),
|
||||
http.Context(ctx),
|
||||
http.Config(cfg),
|
||||
http.Service(svc),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Info().
|
||||
Err(err).
|
||||
Str("transport", "http").
|
||||
Msg("Failed to initialize server")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(func() error {
|
||||
return server.Run()
|
||||
}, func(err error) {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("transport", "http").
|
||||
Msg("Shutting down server")
|
||||
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
server, err := debug.Server(
|
||||
debug.Logger(logger),
|
||||
debug.Context(ctx),
|
||||
debug.Config(cfg),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server")
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(server.ListenAndServe, func(err error) {
|
||||
logger.Error().Err(err)
|
||||
_ = server.Shutdown(ctx)
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
return gr.Run()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getRelationProviders(cfg *config.Config) (map[string]service.RelationProvider, error) {
|
||||
rels := map[string]service.RelationProvider{}
|
||||
for _, relationURI := range cfg.Relations {
|
||||
switch relationURI {
|
||||
case relations.OpenIDConnectRel:
|
||||
rels[relationURI] = relations.OpenIDDiscovery(cfg.IDP)
|
||||
case relations.OwnCloudInstanceRel:
|
||||
var err error
|
||||
rels[relationURI], err = relations.OwnCloudInstance(cfg.Instances, cfg.OcisURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown relation '%s'", relationURI)
|
||||
}
|
||||
}
|
||||
return rels, nil
|
||||
}
|
||||
50
services/webfinger/pkg/command/version.go
Normal file
50
services/webfinger/pkg/command/version.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Version prints the service versions of all running instances.
|
||||
func Version(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "version",
|
||||
Usage: "print the version of this binary and the running service instances",
|
||||
Category: "info",
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Println("Version: " + version.GetString())
|
||||
fmt.Printf("Compiled: %s\n", version.Compiled())
|
||||
fmt.Println("")
|
||||
|
||||
reg := registry.GetRegistry()
|
||||
services, err := reg.GetService(cfg.HTTP.Namespace + "." + cfg.Service.Name)
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(services) == 0 {
|
||||
fmt.Println("No running " + cfg.Service.Name + " service found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
}
|
||||
}
|
||||
table.Render()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
36
services/webfinger/pkg/config/config.go
Normal file
36
services/webfinger/pkg/config/config.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
)
|
||||
|
||||
// Config combines all available configuration parts.
|
||||
type Config struct {
|
||||
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
|
||||
|
||||
Service Service `yaml:"-"`
|
||||
|
||||
Tracing *Tracing `yaml:"tracing"`
|
||||
Log *Log `yaml:"log"`
|
||||
Debug Debug `yaml:"debug"`
|
||||
|
||||
HTTP HTTP `yaml:"http"`
|
||||
|
||||
Instances []Instance `yaml:"instances"`
|
||||
Relations []string `yaml:"relations" env:"WEBFINGER_RELATIONS" desc:"A comma-separated list of relation URIs or registered relation types to add to webfinger responses."`
|
||||
IDP string `yaml:"idp" env:"OCIS_URL;OCIS_OIDC_ISSUER;WEBFINGER_OIDC_ISSUER" desc:"The identity provider href for the openid-discovery relation."`
|
||||
OcisURL string `yaml:"ocis_url" env:"OCIS_URL;WEBFINGER_OWNCLOUD_SERVER_INSTANCE_URL" desc:"The URL for the legacy ownCloud server instance relation (not to be confused with the product ownCloud Server). It defaults to the OCIS_URL but can be overridden to support some reverse proxy corner cases. To shard the deployment, multiple instances can be configured in the configuration file."`
|
||||
|
||||
Context context.Context `yaml:"-"`
|
||||
}
|
||||
|
||||
// Instance to use with a matching rule and titles
|
||||
type Instance struct {
|
||||
Claim string `yaml:"claim"`
|
||||
Regex string `yaml:"regex"`
|
||||
Href string `yaml:"href"`
|
||||
Titles map[string]string `yaml:"titles"`
|
||||
Break bool `yaml:"break"`
|
||||
}
|
||||
9
services/webfinger/pkg/config/debug.go
Normal file
9
services/webfinger/pkg/config/debug.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Debug defines the available debug configuration.
|
||||
type Debug struct {
|
||||
Addr string `yaml:"addr" env:"WEBFINGER_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed."`
|
||||
Token string `yaml:"token" env:"WEBFINGER_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint."`
|
||||
Pprof bool `yaml:"pprof" env:"WEBFINGER_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling."`
|
||||
Zpages bool `yaml:"zpages" env:"WEBFINGER_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces."`
|
||||
}
|
||||
85
services/webfinger/pkg/config/defaults/defaultconfig.go
Normal file
85
services/webfinger/pkg/config/defaults/defaultconfig.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/relations"
|
||||
)
|
||||
|
||||
func FullDefaultConfig() *config.Config {
|
||||
cfg := DefaultConfig()
|
||||
EnsureDefaults(cfg)
|
||||
Sanitize(cfg)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func DefaultConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Debug: config.Debug{
|
||||
Addr: "127.0.0.1:0", // :0 to pick any free local port
|
||||
Token: "",
|
||||
Pprof: false,
|
||||
Zpages: false,
|
||||
},
|
||||
HTTP: config.HTTP{
|
||||
Addr: "127.0.0.1:0", // :0 to pick any free local port
|
||||
Root: "/",
|
||||
Namespace: "com.owncloud.web",
|
||||
CORS: config.CORS{
|
||||
AllowedOrigins: []string{"*"},
|
||||
},
|
||||
},
|
||||
Service: config.Service{
|
||||
Name: "webfinger",
|
||||
},
|
||||
|
||||
Relations: []string{relations.OpenIDConnectRel, relations.OwnCloudInstanceRel},
|
||||
Instances: []config.Instance{
|
||||
{
|
||||
Claim: "sub",
|
||||
Regex: ".+",
|
||||
Href: "{{.OCIS_URL}}",
|
||||
Titles: map[string]string{
|
||||
"en": "oCIS Instance",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func EnsureDefaults(cfg *config.Config) {
|
||||
// provide with defaults for shared logging, since we need a valid destination address for "envdecode".
|
||||
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
|
||||
cfg.Log = &config.Log{
|
||||
Level: cfg.Commons.Log.Level,
|
||||
Pretty: cfg.Commons.Log.Pretty,
|
||||
Color: cfg.Commons.Log.Color,
|
||||
File: cfg.Commons.Log.File,
|
||||
}
|
||||
} else if cfg.Log == nil {
|
||||
cfg.Log = &config.Log{}
|
||||
}
|
||||
// provide with defaults for shared tracing, since we need a valid destination address for "envdecode".
|
||||
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
|
||||
cfg.Tracing = &config.Tracing{
|
||||
Enabled: cfg.Commons.Tracing.Enabled,
|
||||
Type: cfg.Commons.Tracing.Type,
|
||||
Endpoint: cfg.Commons.Tracing.Endpoint,
|
||||
Collector: cfg.Commons.Tracing.Collector,
|
||||
}
|
||||
} else if cfg.Tracing == nil {
|
||||
cfg.Tracing = &config.Tracing{}
|
||||
}
|
||||
|
||||
if cfg.Commons != nil {
|
||||
cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS
|
||||
}
|
||||
}
|
||||
|
||||
func Sanitize(cfg *config.Config) {
|
||||
// sanitize config
|
||||
if cfg.HTTP.Root != "/" {
|
||||
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
|
||||
}
|
||||
}
|
||||
20
services/webfinger/pkg/config/http.go
Normal file
20
services/webfinger/pkg/config/http.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package config
|
||||
|
||||
import "github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
|
||||
// CORS defines the available cors configuration.
|
||||
type CORS struct {
|
||||
AllowedOrigins []string `yaml:"allow_origins" env:"OCIS_CORS_ALLOW_ORIGINS;WEBFINGER_CORS_ALLOW_ORIGINS" desc:"A comma-separated list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin"`
|
||||
AllowedMethods []string `yaml:"allow_methods" env:"OCIS_CORS_ALLOW_METHODS;WEBFINGER_CORS_ALLOW_METHODS" desc:"A comma-separated list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method"`
|
||||
AllowedHeaders []string `yaml:"allow_headers" env:"OCIS_CORS_ALLOW_HEADERS;WEBFINGER_CORS_ALLOW_HEADERS" desc:"A comma-separated list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers."`
|
||||
AllowCredentials bool `yaml:"allow_credentials" env:"OCIS_CORS_ALLOW_CREDENTIALS;WEBFINGER_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials."`
|
||||
}
|
||||
|
||||
// HTTP defines the available http configuration.
|
||||
type HTTP struct {
|
||||
Addr string `yaml:"addr" env:"WEBFINGER_HTTP_ADDR" desc:"The bind address of the HTTP service."`
|
||||
Namespace string `yaml:"-"`
|
||||
Root string `yaml:"root" env:"WEBFINGER_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service."`
|
||||
CORS CORS `yaml:"cors"`
|
||||
TLS shared.HTTPServiceTLS `yaml:"tls"`
|
||||
}
|
||||
9
services/webfinger/pkg/config/log.go
Normal file
9
services/webfinger/pkg/config/log.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Log defines the available log configuration.
|
||||
type Log struct {
|
||||
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;WEBFINGER_LOG_LEVEL" desc:"The log level. Valid values are: \"panic\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\"."`
|
||||
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;WEBFINGER_LOG_PRETTY" desc:"Activates pretty log output."`
|
||||
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;WEBFINGER_LOG_COLOR" desc:"Activates colorized log output."`
|
||||
File string `mapstructure:"file" env:"OCIS_LOG_FILE;WEBFINGER_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set."`
|
||||
}
|
||||
37
services/webfinger/pkg/config/parser/parse.go
Normal file
37
services/webfinger/pkg/config/parser/parse.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config/defaults"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/envdecode"
|
||||
)
|
||||
|
||||
// ParseConfig loads configuration from known paths.
|
||||
func ParseConfig(cfg *config.Config) error {
|
||||
_, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaults.EnsureDefaults(cfg)
|
||||
|
||||
// load all env variables relevant to the config in the current context.
|
||||
if err := envdecode.Decode(cfg); err != nil {
|
||||
// no environment variable set for this config is an expected "error"
|
||||
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defaults.Sanitize(cfg)
|
||||
|
||||
return Validate(cfg)
|
||||
}
|
||||
|
||||
func Validate(cfg *config.Config) error {
|
||||
return nil
|
||||
}
|
||||
6
services/webfinger/pkg/config/service.go
Normal file
6
services/webfinger/pkg/config/service.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
// Service defines the available service configuration.
|
||||
type Service struct {
|
||||
Name string `yaml:"-"`
|
||||
}
|
||||
9
services/webfinger/pkg/config/tracing.go
Normal file
9
services/webfinger/pkg/config/tracing.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Tracing defines the available tracing configuration.
|
||||
type Tracing struct {
|
||||
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;WEBFINGER_TRACING_ENABLED" desc:"Activates tracing."`
|
||||
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;WEBFINGER_TRACING_TYPE" desc:"The type of tracing. Defaults to \"\", which is the same as \"jaeger\". Allowed tracing types are \"jaeger\" and \"\" as of now."`
|
||||
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;WEBFINGER_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent."`
|
||||
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;WEBFINGER_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset."`
|
||||
}
|
||||
17
services/webfinger/pkg/logging/logging.go
Normal file
17
services/webfinger/pkg/logging/logging.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
)
|
||||
|
||||
// LoggerFromConfig initializes a service-specific logger instance.
|
||||
func Configure(name string, cfg *config.Log) log.Logger {
|
||||
return log.NewLogger(
|
||||
log.Name(name),
|
||||
log.Level(cfg.Level),
|
||||
log.Pretty(cfg.Pretty),
|
||||
log.Color(cfg.Color),
|
||||
log.File(cfg.File),
|
||||
)
|
||||
}
|
||||
81
services/webfinger/pkg/metrics/metrics.go
Normal file
81
services/webfinger/pkg/metrics/metrics.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package metrics
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
var (
|
||||
// Namespace defines the namespace for the defines metrics.
|
||||
Namespace = "ocis"
|
||||
|
||||
// Subsystem defines the subsystem for the defines metrics.
|
||||
Subsystem = "webfinger"
|
||||
)
|
||||
|
||||
// Metrics defines the available metrics of this service.
|
||||
type Metrics struct {
|
||||
BuildInfo *prometheus.GaugeVec
|
||||
Counter *prometheus.CounterVec
|
||||
Latency *prometheus.SummaryVec
|
||||
Duration *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
// New initializes the available metrics.
|
||||
func New(opts ...Option) *Metrics {
|
||||
options := newOptions(opts...)
|
||||
|
||||
m := &Metrics{
|
||||
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "build_info",
|
||||
Help: "Build information",
|
||||
}, []string{"version"}),
|
||||
Counter: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "webfinger_total",
|
||||
Help: "How many webfinger requests processed",
|
||||
}, []string{}),
|
||||
Latency: prometheus.NewSummaryVec(prometheus.SummaryOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "webfinger_latency_microseconds",
|
||||
Help: "Webfinger request latencies in microseconds",
|
||||
}, []string{}),
|
||||
Duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "webfinger_duration_seconds",
|
||||
Help: "Webfinger request time in seconds",
|
||||
}, []string{}),
|
||||
}
|
||||
|
||||
if err := prometheus.Register(m.BuildInfo); err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
Str("metric", "BuildInfo").
|
||||
Msg("Failed to register prometheus metric")
|
||||
}
|
||||
|
||||
if err := prometheus.Register(m.Counter); err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
Str("metric", "counter").
|
||||
Msg("Failed to register prometheus metric")
|
||||
}
|
||||
|
||||
if err := prometheus.Register(m.Latency); err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
Str("metric", "latency").
|
||||
Msg("Failed to register prometheus metric")
|
||||
}
|
||||
|
||||
if err := prometheus.Register(m.Duration); err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
Str("metric", "duration").
|
||||
Msg("Failed to register prometheus metric")
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
31
services/webfinger/pkg/metrics/options.go
Normal file
31
services/webfinger/pkg/metrics/options.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
33
services/webfinger/pkg/relations/openid_discovery.go
Normal file
33
services/webfinger/pkg/relations/openid_discovery.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package relations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger"
|
||||
)
|
||||
|
||||
const (
|
||||
OpenIDConnectRel = "http://openid.net/specs/connect/1.0/issuer"
|
||||
)
|
||||
|
||||
type openIDDiscovery struct {
|
||||
Href string
|
||||
}
|
||||
|
||||
// OpenIDDiscovery adds the Openid Connect issuer relation
|
||||
func OpenIDDiscovery(href string) service.RelationProvider {
|
||||
return &openIDDiscovery{
|
||||
Href: href,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *openIDDiscovery) Add(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) {
|
||||
if jrd == nil {
|
||||
jrd = &webfinger.JSONResourceDescriptor{}
|
||||
}
|
||||
jrd.Links = append(jrd.Links, webfinger.Link{
|
||||
Rel: OpenIDConnectRel,
|
||||
Href: l.Href,
|
||||
})
|
||||
}
|
||||
26
services/webfinger/pkg/relations/openid_discovery_test.go
Normal file
26
services/webfinger/pkg/relations/openid_discovery_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package relations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger"
|
||||
)
|
||||
|
||||
func TestOpenidDiscovery(t *testing.T) {
|
||||
provider := OpenIDDiscovery("http://issuer.url")
|
||||
|
||||
jrd := webfinger.JSONResourceDescriptor{}
|
||||
|
||||
provider.Add(context.Background(), &jrd)
|
||||
|
||||
if len(jrd.Links) != 1 {
|
||||
t.Errorf("provider returned wrong number of links: %v, expected 1", len(jrd.Links))
|
||||
}
|
||||
if jrd.Links[0].Href != "http://issuer.url" {
|
||||
t.Errorf("provider returned wrong issuer link href: %v, expected %v", jrd.Links[0].Href, "http://issuer.url")
|
||||
}
|
||||
if jrd.Links[0].Rel != "http://openid.net/specs/connect/1.0/issuer" {
|
||||
t.Errorf("provider returned wrong openid connect rel: %v, expected %v", jrd.Links[0].Href, OpenIDConnectRel)
|
||||
}
|
||||
}
|
||||
87
services/webfinger/pkg/relations/owncloud_instance.go
Normal file
87
services/webfinger/pkg/relations/owncloud_instance.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package relations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger"
|
||||
)
|
||||
|
||||
const (
|
||||
OwnCloudInstanceRel = "http://webfinger.owncloud/rel/server-instance"
|
||||
)
|
||||
|
||||
type compiledInstance struct {
|
||||
config.Instance
|
||||
compiledRegex *regexp.Regexp
|
||||
hrefTemplate *template.Template
|
||||
}
|
||||
|
||||
type ownCloudInstance struct {
|
||||
instances []compiledInstance
|
||||
ocisURL string
|
||||
instanceHost string
|
||||
}
|
||||
|
||||
// OwnCloudInstance adds one or more ownCloud instance relations
|
||||
func OwnCloudInstance(instances []config.Instance, ocisURL string) (service.RelationProvider, error) {
|
||||
compiledInstances := make([]compiledInstance, 0, len(instances))
|
||||
var err error
|
||||
for _, instance := range instances {
|
||||
compiled := compiledInstance{Instance: instance}
|
||||
compiled.compiledRegex, err = regexp.Compile(instance.Regex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compiled.hrefTemplate, err = template.New(instance.Claim + ":" + instance.Regex + ":" + instance.Href).Parse(instance.Href)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compiledInstances = append(compiledInstances, compiled)
|
||||
}
|
||||
|
||||
u, err := url.Parse(ocisURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ownCloudInstance{
|
||||
instances: compiledInstances,
|
||||
ocisURL: ocisURL,
|
||||
instanceHost: u.Host + u.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *ownCloudInstance) Add(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) {
|
||||
if jrd == nil {
|
||||
jrd = &webfinger.JSONResourceDescriptor{}
|
||||
}
|
||||
if claims := oidc.FromContext(ctx); claims != nil {
|
||||
if value, ok := claims[oidc.PreferredUsername].(string); ok {
|
||||
jrd.Subject = "acct:" + value + "@" + l.instanceHost
|
||||
} else if value, ok := claims[oidc.Email].(string); ok {
|
||||
jrd.Subject = "mailto:" + value
|
||||
}
|
||||
// allow referencing OCIS_URL in the template
|
||||
claims["OCIS_URL"] = l.ocisURL
|
||||
for _, instance := range l.instances {
|
||||
if value, ok := claims[instance.Claim].(string); ok && instance.compiledRegex.MatchString(value) {
|
||||
var tmplWriter strings.Builder
|
||||
instance.hrefTemplate.Execute(&tmplWriter, claims)
|
||||
jrd.Links = append(jrd.Links, webfinger.Link{
|
||||
Rel: OwnCloudInstanceRel,
|
||||
Href: tmplWriter.String(),
|
||||
Titles: instance.Titles,
|
||||
})
|
||||
if instance.Break {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
services/webfinger/pkg/relations/owncloud_instance_test.go
Normal file
65
services/webfinger/pkg/relations/owncloud_instance_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package relations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger"
|
||||
)
|
||||
|
||||
func TestOwnCloudInstanceErr(t *testing.T) {
|
||||
_, err := OwnCloudInstance([]config.Instance{}, "http://\n\rinvalid")
|
||||
if err == nil {
|
||||
t.Errorf("provider did not err on invalid url: %v", err)
|
||||
}
|
||||
_, err = OwnCloudInstance([]config.Instance{{Regex: "("}}, "http://docis.tld")
|
||||
if err == nil {
|
||||
t.Errorf("provider did not err on invalid regex: %v", err)
|
||||
}
|
||||
_, err = OwnCloudInstance([]config.Instance{{Href: "{{invalid}}ee"}}, "http://docis.tld")
|
||||
if err == nil {
|
||||
t.Errorf("provider did not err on invalid href template: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnCloudInstanceAddLink(t *testing.T) {
|
||||
provider, err := OwnCloudInstance([]config.Instance{{
|
||||
Claim: "customclaim",
|
||||
Regex: ".+@.+\\..+",
|
||||
Href: "https://{{.otherclaim}}.domain.tld",
|
||||
Titles: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
Break: true,
|
||||
}}, "http://docis.tld")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = oidc.NewContext(ctx, map[string]interface{}{
|
||||
"customclaim": "some@fizz.buzz",
|
||||
"otherclaim": "someone",
|
||||
})
|
||||
jrd := webfinger.JSONResourceDescriptor{}
|
||||
provider.Add(ctx, &jrd)
|
||||
|
||||
if len(jrd.Links) != 1 {
|
||||
t.Errorf("provider returned wrong number of links: %v, expected 1", len(jrd.Links))
|
||||
}
|
||||
if jrd.Links[0].Href != "https://someone.domain.tld" {
|
||||
t.Errorf("provider returned wrong issuer link href: %v, expected %v", jrd.Links[0].Href, "https://someone.domain.tld")
|
||||
}
|
||||
if jrd.Links[0].Rel != OwnCloudInstanceRel {
|
||||
t.Errorf("provider returned owncloud server instance rel: %v, expected %v", jrd.Links[0].Rel, OwnCloudInstanceRel)
|
||||
}
|
||||
if len(jrd.Links[0].Titles) != 1 {
|
||||
t.Errorf("provider returned wrong number of titles: %v, expected 1", len(jrd.Links[0].Titles))
|
||||
}
|
||||
if jrd.Links[0].Titles["foo"] != "bar" {
|
||||
t.Errorf("provider returned wrong title: %v, expected bar", len(jrd.Links[0].Titles["foo"]))
|
||||
}
|
||||
|
||||
}
|
||||
50
services/webfinger/pkg/server/debug/option.go
Normal file
50
services/webfinger/pkg/server/debug/option.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
63
services/webfinger/pkg/server/debug/server.go
Normal file
63
services/webfinger/pkg/server/debug/server.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/debug"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
)
|
||||
|
||||
// Server initializes the debug service and server.
|
||||
func Server(opts ...Option) (*http.Server, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
return debug.NewService(
|
||||
debug.Logger(options.Logger),
|
||||
debug.Name(options.Config.Service.Name),
|
||||
debug.Version(version.GetString()),
|
||||
debug.Address(options.Config.Debug.Addr),
|
||||
debug.Token(options.Config.Debug.Token),
|
||||
debug.Pprof(options.Config.Debug.Pprof),
|
||||
debug.Zpages(options.Config.Debug.Zpages),
|
||||
debug.Health(health(options.Config)),
|
||||
debug.Ready(ready(options.Config)),
|
||||
debug.CorsAllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
|
||||
debug.CorsAllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
|
||||
debug.CorsAllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
|
||||
debug.CorsAllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
|
||||
), nil
|
||||
}
|
||||
|
||||
// health implements the health check.
|
||||
func health(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// TODO: check if services are up and running
|
||||
|
||||
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
|
||||
// io.WriteString should not fail but if it does we want to know.
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ready implements the ready check.
|
||||
func ready(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// TODO: check if services are up and running
|
||||
|
||||
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
|
||||
// io.WriteString should not fail but if it does we want to know.
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
84
services/webfinger/pkg/server/http/option.go
Normal file
84
services/webfinger/pkg/server/http/option.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
svc "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
Flags []cli.Flag
|
||||
Service svc.Service
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Name provides a name for the service.
|
||||
func Name(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Name = val
|
||||
}
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// Flags provides a function to set the flags option.
|
||||
func Flags(val []cli.Flag) Option {
|
||||
return func(o *Options) {
|
||||
o.Flags = append(o.Flags, val...)
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace provides a function to set the namespace option.
|
||||
func Namespace(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = val
|
||||
}
|
||||
}
|
||||
|
||||
// Service provides a function to set the service option.
|
||||
func Service(val svc.Service) Option {
|
||||
return func(o *Options) {
|
||||
o.Service = val
|
||||
}
|
||||
}
|
||||
132
services/webfinger/pkg/server/http/server.go
Normal file
132
services/webfinger/pkg/server/http/server.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/cors"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
|
||||
ohttp "github.com/owncloud/ocis/v2/ocis-pkg/service/http"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
serviceErrors "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0"
|
||||
svc "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0"
|
||||
"github.com/pkg/errors"
|
||||
"go-micro.dev/v4"
|
||||
)
|
||||
|
||||
// Server initializes the http service and server.
|
||||
func Server(opts ...Option) (ohttp.Service, error) {
|
||||
options := newOptions(opts...)
|
||||
service := options.Service
|
||||
|
||||
svc, err := ohttp.NewService(
|
||||
ohttp.TLSConfig(options.Config.HTTP.TLS),
|
||||
ohttp.Logger(options.Logger),
|
||||
ohttp.Namespace(options.Config.HTTP.Namespace),
|
||||
ohttp.Name(options.Config.Service.Name),
|
||||
ohttp.Version(version.GetString()),
|
||||
ohttp.Address(options.Config.HTTP.Addr),
|
||||
ohttp.Context(options.Context),
|
||||
ohttp.Flags(options.Flags...),
|
||||
)
|
||||
if err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
Msg("Error initializing http service")
|
||||
return ohttp.Service{}, err
|
||||
}
|
||||
|
||||
mux := chi.NewMux()
|
||||
|
||||
mux.Use(chimiddleware.RealIP)
|
||||
mux.Use(chimiddleware.RequestID)
|
||||
mux.Use(middleware.TraceContext)
|
||||
mux.Use(middleware.NoCache)
|
||||
mux.Use(
|
||||
middleware.Cors(
|
||||
cors.Logger(options.Logger),
|
||||
cors.AllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
|
||||
cors.AllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
|
||||
cors.AllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
|
||||
cors.AllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
|
||||
))
|
||||
mux.Use(middleware.Secure)
|
||||
|
||||
mux.Use(middleware.Version(
|
||||
options.Name,
|
||||
version.String,
|
||||
))
|
||||
|
||||
mux.Use(middleware.OidcAuth(
|
||||
middleware.WithLogger(options.Logger),
|
||||
middleware.WithOidcIssuer(options.Config.IDP),
|
||||
))
|
||||
|
||||
// this logs http request related data
|
||||
mux.Use(middleware.Logger(
|
||||
options.Logger,
|
||||
))
|
||||
|
||||
mux.Route(options.Config.HTTP.Root, func(r chi.Router) {
|
||||
r.Get("/.well-known/webfinger", WebfingerHandler(service))
|
||||
})
|
||||
|
||||
err = micro.RegisterHandler(svc.Server(), mux)
|
||||
if err != nil {
|
||||
options.Logger.Fatal().Err(err).Msg("failed to register the handler")
|
||||
}
|
||||
|
||||
svc.Init()
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func WebfingerHandler(service svc.Service) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// A WebFinger URI MUST contain a query component (see Section 3.4 of
|
||||
// RFC 3986). The query component MUST contain a "resource" parameter
|
||||
// and MAY contain one or more "rel" parameters.
|
||||
resource := r.URL.Query().Get("resource")
|
||||
queryTarget, err := url.Parse(resource)
|
||||
if resource == "" || err != nil {
|
||||
// If the "resource" parameter is absent or malformed, the WebFinger
|
||||
// resource MUST indicate that the request is bad as per Section 10.4.1
|
||||
// of RFC 2616.
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.PlainText(w, r, "absent or malformed 'resource' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
rels := make([]string, 0)
|
||||
for k, v := range r.URL.Query() {
|
||||
if k == "rel" {
|
||||
rels = append(rels, v...)
|
||||
}
|
||||
}
|
||||
|
||||
jrd, err := service.Webfinger(ctx, queryTarget, rels)
|
||||
if errors.Is(err, serviceErrors.ErrNotFound) {
|
||||
// from https://www.rfc-editor.org/rfc/rfc7033#section-4.2
|
||||
//
|
||||
// If the "resource" parameter is a value for which the server has no
|
||||
// information, the server MUST indicate that it was unable to match the
|
||||
// request as per Section 10.4.5 of RFC 2616.
|
||||
render.Status(r, http.StatusNotFound)
|
||||
render.PlainText(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
render.PlainText(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-type", "application/jrd+json")
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, jrd)
|
||||
}
|
||||
}
|
||||
5
services/webfinger/pkg/service/v0/errors.go
Normal file
5
services/webfinger/pkg/service/v0/errors.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package service
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrNotFound = errors.New("query target not found")
|
||||
39
services/webfinger/pkg/service/v0/instrument.go
Normal file
39
services/webfinger/pkg/service/v0/instrument.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/metrics"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// NewInstrument returns a service that instruments metrics.
|
||||
func NewInstrument(next Service, metrics *metrics.Metrics) Service {
|
||||
return instrument{
|
||||
next: next,
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
type instrument struct {
|
||||
next Service
|
||||
metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// Webfinger implements the Service interface.
|
||||
func (i instrument) Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) {
|
||||
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
|
||||
us := v * 1000000
|
||||
|
||||
i.metrics.Latency.WithLabelValues().Observe(us)
|
||||
i.metrics.Duration.WithLabelValues().Observe(v)
|
||||
}))
|
||||
|
||||
defer timer.ObserveDuration()
|
||||
|
||||
i.metrics.Counter.WithLabelValues().Inc()
|
||||
|
||||
return i.next.Webfinger(ctx, queryTarget, rels)
|
||||
}
|
||||
32
services/webfinger/pkg/service/v0/logging.go
Normal file
32
services/webfinger/pkg/service/v0/logging.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger"
|
||||
)
|
||||
|
||||
// NewLogging returns a service that logs messages.
|
||||
func NewLogging(next Service, logger log.Logger) Service {
|
||||
return logging{
|
||||
next: next,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type logging struct {
|
||||
next Service
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// Webfinger implements the Service interface.
|
||||
func (l logging) Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) {
|
||||
l.logger.Debug().
|
||||
Str("query_target", queryTarget.String()).
|
||||
Strs("rel", rels).
|
||||
Msg("Webfinger")
|
||||
|
||||
return l.next.Webfinger(ctx, queryTarget, rels)
|
||||
}
|
||||
47
services/webfinger/pkg/service/v0/option.go
Normal file
47
services/webfinger/pkg/service/v0/option.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
RelationProviders map[string]RelationProvider
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithRelationProviders(val map[string]RelationProvider) Option {
|
||||
return func(o *Options) {
|
||||
o.RelationProviders = val
|
||||
}
|
||||
}
|
||||
110
services/webfinger/pkg/service/v0/service.go
Normal file
110
services/webfinger/pkg/service/v0/service.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger"
|
||||
)
|
||||
|
||||
const (
|
||||
OwnCloudInstanceRel = "http://webfinger.owncloud/rel/server-instance"
|
||||
OpenIDConnectRel = "http://openid.net/specs/connect/1.0/issuer"
|
||||
)
|
||||
|
||||
// Service defines the extension handlers.
|
||||
type Service interface {
|
||||
// Webfinger is the endpoint for retrieving various href relations.
|
||||
//
|
||||
// GET /.well-known/webfinger?
|
||||
// resource=acct%3Acarol%40example.com&
|
||||
// rel=http%3A%2F%2Fwebfinger.owncloud%rel%2Fserver-instance
|
||||
// HTTP/1.1
|
||||
// Host: example.com
|
||||
//
|
||||
// The server might respond like this:
|
||||
//
|
||||
// HTTP/1.1 200 OK
|
||||
// Access-Control-Allow-Origin: *
|
||||
// Content-Type: application/jrd+json
|
||||
//
|
||||
// {
|
||||
// "subject" : "acct:carol@example.com",
|
||||
// "links" :
|
||||
// [
|
||||
// {
|
||||
// "rel" : "http://webfinger.owncloud/rel/server-instance",
|
||||
// "href" : "https://instance.example.com",
|
||||
// "titles": {
|
||||
// "en": "Readable Instance Name"
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "rel" : "http://webfinger.owncloud/rel/server-instance",
|
||||
// "href" : "https://otherinstance.example.com",
|
||||
// "titles": {
|
||||
// "en": "Other Readable Instance Name"
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error)
|
||||
}
|
||||
|
||||
type RelationProvider interface {
|
||||
Add(ctx context.Context, jrd *webfinger.JSONResourceDescriptor)
|
||||
}
|
||||
|
||||
// New returns a new instance of Service
|
||||
func New(opts ...Option) (Service, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
// TODO use fallback implementations of InstanceIdLookup and InstanceLookup?
|
||||
// The InstanceIdLookup may have to happen earlier?
|
||||
|
||||
return svc{
|
||||
log: options.Logger,
|
||||
config: options.Config,
|
||||
relationProviders: options.RelationProviders,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type svc struct {
|
||||
config *config.Config
|
||||
log log.Logger
|
||||
relationProviders map[string]RelationProvider
|
||||
}
|
||||
|
||||
// TODO implement different implementations:
|
||||
// static one returning the href or a configureable domain
|
||||
// regex one returning different instances based on the regex that matches
|
||||
// claim one that reads a claim and then fetches the instance?
|
||||
// that is actually two interfaces / steps:
|
||||
// - one that determines the instances/schools id (read from claim, regex match)
|
||||
// - one that looks up in instance by id (use template, read from json, read from ldap, read from graph)
|
||||
|
||||
// Webfinger implements the service interface
|
||||
func (s svc) Webfinger(ctx context.Context, queryTarget *url.URL, rel []string) (webfinger.JSONResourceDescriptor, error) {
|
||||
|
||||
jrd := webfinger.JSONResourceDescriptor{
|
||||
Subject: queryTarget.String(),
|
||||
}
|
||||
|
||||
if len(rel) == 0 {
|
||||
// add all configured relation providers
|
||||
for _, relation := range s.relationProviders {
|
||||
relation.Add(ctx, &jrd)
|
||||
}
|
||||
} else {
|
||||
// only add requested relations
|
||||
for _, r := range rel {
|
||||
if relation, ok := s.relationProviders[r]; ok {
|
||||
relation.Add(ctx, &jrd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return jrd, nil
|
||||
}
|
||||
33
services/webfinger/pkg/service/v0/tracing.go
Normal file
33
services/webfinger/pkg/service/v0/tracing.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
webfingertracing "github.com/owncloud/ocis/v2/services/webfinger/pkg/tracing"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// NewTracing returns a service that instruments traces.
|
||||
func NewTracing(next Service) Service {
|
||||
return tracing{
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
type tracing struct {
|
||||
next Service
|
||||
}
|
||||
|
||||
// Webfinger implements the Service interface.
|
||||
func (t tracing) Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) {
|
||||
ctx, span := webfingertracing.TraceProvider.Tracer("webfinger").Start(ctx, "Webfinger", trace.WithAttributes(
|
||||
attribute.KeyValue{Key: "query_target", Value: attribute.StringValue(queryTarget.String())},
|
||||
attribute.KeyValue{Key: "rels", Value: attribute.StringSliceValue(rels)},
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return t.next.Webfinger(ctx, queryTarget, rels)
|
||||
}
|
||||
23
services/webfinger/pkg/tracing/tracing.go
Normal file
23
services/webfinger/pkg/tracing/tracing.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
pkgtrace "github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
// TraceProvider is the global trace provider for the proxy service.
|
||||
TraceProvider = trace.NewNoopTracerProvider()
|
||||
)
|
||||
|
||||
func Configure(cfg *config.Config) error {
|
||||
var err error
|
||||
if cfg.Tracing.Enabled {
|
||||
if TraceProvider, err = pkgtrace.GetTraceProvider(cfg.Tracing.Endpoint, cfg.Tracing.Collector, cfg.Service.Name, cfg.Tracing.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
64
services/webfinger/pkg/webfinger/webfinger.go
Normal file
64
services/webfinger/pkg/webfinger/webfinger.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package webfinger
|
||||
|
||||
// Link represents a link relation object as per https://www.rfc-editor.org/rfc/rfc7033#section-4.4.4
|
||||
type Link struct {
|
||||
// Rel is either a URI or a registered relation type (see RFC 5988)
|
||||
//
|
||||
// The "rel" member MUST be present in the link relation object.
|
||||
Rel string `json:"rel"`
|
||||
// Type indicates the media type of the target resource
|
||||
//
|
||||
// The "type" member is OPTIONAL in the link relation object.
|
||||
Type string `json:"type,omitempty"`
|
||||
// Href contains a URI pointing to the target resource.
|
||||
//
|
||||
// The "href" member is OPTIONAL in the link relation object.
|
||||
Href string `json:"href,omitempty"`
|
||||
// The "properties" object within the link relation object comprises
|
||||
// zero or more name/value pairs whose names are URIs (referred to as
|
||||
// "property identifiers") and whose values are strings or null.
|
||||
//
|
||||
// Properties are used to convey additional information about the link
|
||||
// relation. As an example, consider this use of "properties":
|
||||
//
|
||||
// "properties" : { "http://webfinger.example/mail/port" : "993" }
|
||||
//
|
||||
// The "properties" member is OPTIONAL in the link relation object.
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
// Titles comprises zero or more name/value pairs whose
|
||||
// names are a language tag or the string "und"
|
||||
//
|
||||
// Here is an example of the "titles" object:
|
||||
//
|
||||
// "titles" :
|
||||
// {
|
||||
// "en-us" : "The Magical World of Steve",
|
||||
// "fr" : "Le Monde Magique de Steve"
|
||||
// }
|
||||
//
|
||||
// The "titles" member is OPTIONAL in the link relation object.
|
||||
Titles map[string]string `json:"titles,omitempty"`
|
||||
}
|
||||
|
||||
// JSONResourceDescriptor represents a JSON Resource Descriptor (JRD) as per https://www.rfc-editor.org/rfc/rfc7033#section-4.4
|
||||
type JSONResourceDescriptor struct {
|
||||
// Subject is a URI that identifies the entity that the JRD describes
|
||||
//
|
||||
// The "subject" member SHOULD be present in the JRD.
|
||||
Subject string `json:"subject,omitempty"`
|
||||
// Aliases is an array of zero or more URI strings that identify the same
|
||||
// entity as the "subject" URI.
|
||||
//
|
||||
// The "aliases" array is OPTIONAL in the JRD.
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
// Properties is an object comprising zero or more name/value pairs whose
|
||||
// names are URIs (referred to as "property identifiers") and whose
|
||||
// values are strings or null.
|
||||
//
|
||||
// The "properties" member is OPTIONAL in the JRD.
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
// Links is an array of objects that contain link relation information
|
||||
//
|
||||
// The "links" array is OPTIONAL in the JRD.
|
||||
Links []Link `json:"links,omitempty"`
|
||||
}
|
||||
2
services/webfinger/reflex.conf
Normal file
2
services/webfinger/reflex.conf
Normal file
@@ -0,0 +1,2 @@
|
||||
# backend
|
||||
-r '^(cmd|pkg)/.*\.go$' -R '^node_modules/' -s -- sh -c 'make bin/ocis-webfinger-debug && bin/ocis-webfinger-debug --log-level debug server --debug-pprof --debug-zpages'
|
||||
Reference in New Issue
Block a user