Merge pull request #7299 from ywk253100/190404_sync

Sync with master branch
This commit is contained in:
Wenkai Yin 2019-04-04 17:33:11 +08:00 committed by GitHub
commit 58a73de3e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
730 changed files with 288257 additions and 522 deletions

View File

@ -42,7 +42,6 @@ Here is the basic structure of the harbor code base. Some of the key folders / f
```
.
...
├── Makefile # Make file for compile and build code
├── contrib # Contain documents, scripts, and other helpful things which are contributed by the community
├── docs # Keep documents here
├── make # Resource for building and setting up Harbor environment
@ -56,46 +55,56 @@ Here is the basic structure of the harbor code base. Some of the key folders / f
The folder graph below shows the structure of the source code folder `harbor/src`, which will be your primary working directory. The key folders are also commented.
```
.
├── adminserver # Source code for the admin server component
│   ├── api
│   ├── auth
│   ├── client
│   ├── handlers
│   ├── systemcfg
│   └── systeminfo
├── chartserver # Source code contains the main logic to handle chart.
├── cmd # Source code contains migrate script to handle DB upgrade.
├── common # Source code for some general components like dao etc.
│   ├── api
│   ├── config
│   ├── dao
│   ├── http
│   ├── job
│   ├── models
│   ├── notifier
│   ├── scheduler
│   ├── rbac
│   ├── registryctl
│   ├── secret
│   ├── security
│   └── utils
├── jobservice # Source code for the job service component
│   ├── api
│   ├── config
│   ├── job
│   ├── replication
│   ├── scan
│   └── utils
├── ui # Source code for the harbor service component
├── core # Source code for the main busines logic. Contains rest apis and all service infomation.
│   ├── api
│   ├── auth
│   ├── config
│   ├── controllers
│   ├── filter
│   ├── label
│   ├── notifier
│   ├── promgr
│   ├── proxy
│   ├── service
│   ├── static
│   ├── systeminfo
│   ├── utils
│   └── views
├── jobservice # Source code for the job service component
│   ├── api
│   ├── config
│   ├── core
│   ├── env
│   ├── errs
│   ├── job
│   ├── logger
│   ├── models
│   ├── opm
│   ├── period
│   ├── pool
│   ├── runtime
│   ├── tests
│   └── utils
├── portal # The code of harbor web UI
│   ├── e2e
│   ├── lib # Source code of @harbor/ui npm library which includes the main UI components of web UI
│   └── src # General web page UI code of Harbor
├── registryctl # Source code contains the main logic to handle registry.
├── replication # Source code contains the main logic of replication.
├── testing # Some utilities to handle testing.
└── vendor # Go code dependencies
├── github.com
├── golang.org
@ -116,7 +125,8 @@ Harbor backend is written in [Go](http://golang.org/). If you don't have a Harbo
| 1.4 | 1.9.2 |
| 1.5 | 1.9.2 |
| 1.6 | 1.9.2 |
| 1.6 | tbd |
| 1.7 | 1.9.2 |
| 1.8 | 1.11.2 |
Ensure your GOPATH and PATH have been configured in accordance with the Go environment instructions.
@ -134,6 +144,8 @@ Harbor web UI is built based on [Clarity](https://vmware.github.io/clarity/) and
| 1.4 | 4.3.0 | 0.10.17 |
| 1.5 | 4.3.0 | 0.10.27 |
| 1.6 | 4.3.0 | 0.10.27 |
| 1.7 | 6.0.3 | 0.12.10 |
| 1.8 | 7.1.3 | 1.0.0 |
**npm Package Dependency:** Run the following commands to restore the package dependencies.
```
@ -155,6 +167,8 @@ PR are always welcome, even if they only contain small fixes like typos or a few
Please submit a PR broken down into small changes bit by bit. A PR consisting of a lot features and code changes may be hard to review. It is recommended to submit PRs in an incremental fashion.
Note: If you split your pull request to small changes, please make sure any of the changes goes to master will not break anything. Otherwise, it can not be merged until this feature complete.
The graphic shown below describes the overall workflow about how to contribute code to Harbor repository.
![contribute workflow](docs/img/workflow.png)
@ -316,5 +330,4 @@ Documents are written with Markdown text. See [Writing on GitHub](https://help.g
## Design new features
You can propose new designs for existing Harbor features. You can also design
entirely new features. Please do open an issue on Github for discussion first. This is necessary to ensure the overall architecture is consistent and to avoid duplicated work in the roadmap.
You can propose new designs for existing Harbor features. You can also design entirely new features, Please submit a proposal in GitHub.(https://github.com/goharbor/community/tree/master/proposals). Harbor maintainers will review this proposal as soon as possible. This is necessary to ensure the overall architecture is consistent and to avoid duplicated work in the roadmap.

View File

@ -32,7 +32,7 @@ Use Swagger to find out the specs of Harbor API.
[Internationalization Guide](developer_guide_i18n.md)
How to add your local language to Harbor.
[Python SDK](../contrib/sdk/harbor-py) (by community)
[Python SDK](../contrib/registryapi) (by community)
[Harbor CLI](https://github.com/int32bit/harborclient) (by community)

View File

@ -13,6 +13,25 @@ CREATE TABLE robot (
CREATE TRIGGER robot_update_time_at_modtime BEFORE UPDATE ON robot FOR EACH ROW EXECUTE PROCEDURE update_update_time_at_column();
CREATE TABLE oidc_user (
id SERIAL NOT NULL,
user_id int NOT NULL,
secret varchar(255) NOT NULL,
/*
Subject and Issuer
Subject: Subject Identifier.
Issuer: Issuer Identifier for the Issuer of the response.
The sub (subject) and iss (issuer) Claims, used together, are the only Claims that an RP can rely upon as a stable identifier for the End-User
*/
subiss varchar(255) NOT NULL,
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE (subiss)
);
CREATE TRIGGER odic_user_update_time_at_modtime BEFORE UPDATE ON oidc_user FOR EACH ROW EXECUTE PROCEDURE update_update_time_at_column();
/*add master role*/
INSERT INTO role (role_code, name) VALUES ('DRWS', 'master');

View File

@ -146,27 +146,16 @@ def parse_yaml_config(config_file_path):
config_dict['admiral_url'] = ""
# Clair configs
clair_configs = configs.get("clair")
if clair_configs:
config_dict['clair_db_password'] = clair_configs.get("db_password")
config_dict['clair_db_host'] = clair_configs.get("db_host")
config_dict['clair_db_port'] = clair_configs.get("db_port")
config_dict['clair_db_username'] = clair_configs.get("db_username")
config_dict['clair_db'] = clair_configs.get("db")
config_dict['clair_updaters_interval'] = clair_configs.get("updaters_interval")
config_dict['clair_http_proxy'] = clair_configs.get('http_proxy')
config_dict['clair_https_proxy'] = clair_configs.get('https_proxy')
config_dict['clair_no_proxy'] = clair_configs.get('no_proxy')
else:
config_dict['clair_db_password'] = ''
config_dict['clair_db_host'] = ''
config_dict['clair_db_port'] = ''
config_dict['clair_db_username'] = ''
config_dict['clair_db'] = ''
config_dict['clair_updaters_interval'] = ''
config_dict['clair_http_proxy'] = ''
config_dict['clair_https_proxy'] = ''
config_dict['clair_no_proxy'] = ''
clair_configs = configs.get("clair") or {}
config_dict['clair_db_password'] = clair_configs.get("db_password") or ''
config_dict['clair_db_host'] = clair_configs.get("db_host") or ''
config_dict['clair_db_port'] = clair_configs.get("db_port") or ''
config_dict['clair_db_username'] = clair_configs.get("db_username") or ''
config_dict['clair_db'] = clair_configs.get("db") or ''
config_dict['clair_updaters_interval'] = clair_configs.get("updaters_interval") or ''
config_dict['clair_http_proxy'] = clair_configs.get('http_proxy') or ''
config_dict['clair_https_proxy'] = clair_configs.get('https_proxy') or ''
config_dict['clair_no_proxy'] = clair_configs.get('no_proxy') or ''
# UAA configs
config_dict['uaa_endpoint'] = configs.get("uaa_endpoint")

View File

@ -76,19 +76,14 @@ def prepare_env_notary(customize_crt, nginx_config_dir):
os.path.join(notary_config_dir, "server_env"))
print("Copying nginx configuration file for notary")
notary_nginx_upstream_template_conf = os.path.join(templates_dir, "nginx", "notary.upstream.conf.jinja")
notary_server_nginx_config = os.path.join(nginx_config_dir, "notary.server.conf")
shutil.copy2(notary_nginx_upstream_template_conf, notary_server_nginx_config)
shutil.copy2(
os.path.join(templates_dir, "nginx", "notary.upstream.conf.jinja"),
os.path.join(nginx_config_dir, "notary.upstream.conf"))
mark_file(os.path.join(notary_secret_dir, "notary-signer.crt"))
mark_file(os.path.join(notary_secret_dir, "notary-signer.key"))
mark_file(os.path.join(notary_secret_dir, "notary-signer-ca.crt"))
# print("Copying sql file for notary DB")
# if os.path.exists(os.path.join(notary_config_dir, "postgresql-initdb.d")):
# shutil.rmtree(os.path.join(notary_config_dir, "postgresql-initdb.d"))
# shutil.copytree(os.path.join(notary_temp_dir, "postgresql-initdb.d"), os.path.join(notary_config_dir, "postgresql-initdb.d"))
def prepare_notary(config_dict, nginx_config_dir, ssl_cert_path, ssl_cert_key_path):

210
src/Gopkg.lock generated
View File

@ -100,6 +100,14 @@
revision = "542e16cac74562eefac970a7d0d1467640d1f1cb"
version = "v1.7.0"
[[projects]]
digest = "1:f6e5e1bc64c2908167e6aa9a1fe0c084d515132a1c63ad5b6c84036aa06dc0c1"
name = "github.com/coreos/go-oidc"
packages = ["."]
pruneopts = "UT"
revision = "1180514eaf4d9f38d0d19eef639a1d695e066e72"
version = "v2.0.0"
[[projects]]
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
name = "github.com/davecgh/go-spew"
@ -235,6 +243,17 @@
revision = "1d4117a214abff263b472043871c8666aedb716b"
version = "v0.5.1"
[[projects]]
digest = "1:4d02824a56d268f74a6b6fdd944b20b58a77c3d70e81008b3ee0c4f1a6777340"
name = "github.com/gogo/protobuf"
packages = [
"proto",
"sortkeys",
]
pruneopts = "UT"
revision = "ba06b47c162d49f2af050fb4c75bcbc86a159d5c"
version = "v1.2.1"
[[projects]]
digest = "1:39d9284259004077d3b89109d592fce5f311788745ce94a7ccd4545e536ad3ac"
name = "github.com/golang-migrate/migrate"
@ -249,6 +268,14 @@
revision = "bcd996f3df28363f43e2d0935484c4559537a3eb"
version = "v3.3.0"
[[projects]]
branch = "master"
digest = "1:1ba1d79f2810270045c328ae5d674321db34e3aae468eb4233883b473c5c0467"
name = "github.com/golang/glog"
packages = ["."]
pruneopts = "UT"
revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998"
[[projects]]
digest = "1:41e5cefde26c58f1560df2d1c32c2fa85e332d7cb4460d2077ae8fd8e0f3d789"
name = "github.com/golang/protobuf"
@ -279,6 +306,14 @@
pruneopts = "UT"
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
[[projects]]
branch = "master"
digest = "1:3ee90c0d94da31b442dde97c99635aaafec68d0b8a3c12ee2075c6bdabeec6bb"
name = "github.com/google/gofuzz"
packages = ["."]
pruneopts = "UT"
revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1"
[[projects]]
digest = "1:160eabf7a69910fd74f29c692718bc2437c1c1c7d4c9dea9712357752a70e5df"
name = "github.com/gorilla/context"
@ -303,6 +338,14 @@
revision = "7f08801859139f86dfafd1c296e2cba9a80d292e"
version = "v1.6.0"
[[projects]]
digest = "1:f5a2051c55d05548d2d4fd23d244027b59fbd943217df8aa3b5e170ac2fd6e1b"
name = "github.com/json-iterator/go"
packages = ["."]
pruneopts = "UT"
revision = "0ff49de124c6f76f8494e194af75bde0f1a49a29"
version = "v1.1.6"
[[projects]]
branch = "master"
digest = "1:bd26bbaf1e9f9dfe829a88f87a0849b56f717c31785443a67668f2c752fa8412"
@ -329,6 +372,22 @@
pruneopts = "UT"
revision = "7283ca79f35edb89bc1b4ecae7f86a3680ce737f"
[[projects]]
digest = "1:33422d238f147d247752996a26574ac48dcf472976eda7f5134015f06bf16563"
name = "github.com/modern-go/concurrent"
packages = ["."]
pruneopts = "UT"
revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94"
version = "1.0.3"
[[projects]]
digest = "1:e32bdbdb7c377a07a9a46378290059822efdce5c8d96fe71940d87cb4f918855"
name = "github.com/modern-go/reflect2"
packages = ["."]
pruneopts = "UT"
revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd"
version = "1.0.1"
[[projects]]
digest = "1:159d8a990f45d4891f1f04cb6ad7eb18b307cd02d783f7d37fa7a3b93912b172"
name = "github.com/opencontainers/go-digest"
@ -353,6 +412,17 @@
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:bd9efe4e0b0f768302a1e2f0c22458149278de533e521206e5ddc71848c269a0"
name = "github.com/pquerna/cachecontrol"
packages = [
".",
"cacheobject",
]
pruneopts = "UT"
revision = "1555304b9b35fdd2b425bccf1a5613677705e7d0"
[[projects]]
digest = "1:3f68283c56d93b885f33c679708079e834815138649e9f59ffbc572c2993e0f8"
name = "github.com/robfig/cron"
@ -382,10 +452,12 @@
version = "v1.2.0"
[[projects]]
digest = "1:9c94d918a2ac65f60d6b7895b2e9612e4554b40ee2446f2f807cadb3e57da7e2"
digest = "1:ab3259b9f5008a18ff8c1cc34623eccce354f3a9faf5b409983cd6717d64b40b"
name = "golang.org/x/crypto"
packages = [
"cast5",
"ed25519",
"ed25519/internal/edwards25519",
"openpgp",
"openpgp/armor",
"openpgp/clearsign",
@ -400,11 +472,14 @@
revision = "5f961cd492ac9d43fc33a8ef646bae79d113fd97"
[[projects]]
digest = "1:3373df9a79dbfdec0111498a2358444eee9f70c3748ee0e0c2a1ad993978f676"
digest = "1:2a465dcd21dc1094bd90bc28adc168d5c12d4d754b49d67b34362d26bd5c21b2"
name = "golang.org/x/net"
packages = [
"context",
"context/ctxhttp",
"http2",
"http2/hpack",
"lex/httplex",
]
pruneopts = "UT"
revision = "075e191f18186a8ff2becaf64478e30f4545cdad"
@ -428,6 +503,14 @@
pruneopts = "UT"
revision = "571f7bbbe08da2a8955aed9d4db316e78630e9a3"
[[projects]]
branch = "master"
digest = "1:9fdc2b55e8e0fafe4b41884091e51e77344f7dc511c5acedcfd98200003bff90"
name = "golang.org/x/time"
packages = ["rate"]
pruneopts = "UT"
revision = "9d24e82272b4f38b78bc8cff74fa936d31ccd8ef"
[[projects]]
digest = "1:52133d6859535332391e6193c8878d06347f28881111efa900392802485e9a18"
name = "google.golang.org/appengine"
@ -451,6 +534,14 @@
revision = "4e86f4367175e39f69d9358a5f17b4dda270378d"
version = "v1.1"
[[projects]]
digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a"
name = "gopkg.in/inf.v0"
packages = ["."]
pruneopts = "UT"
revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf"
version = "v0.9.1"
[[projects]]
digest = "1:79691acfc86fc3204928daf67e44955e8021ec5e10091599d344b0e16de32236"
name = "gopkg.in/ldap.v2"
@ -459,6 +550,18 @@
revision = "8168ee085ee43257585e50c6441aadf54ecb2c9f"
version = "v2.5.0"
[[projects]]
digest = "1:c0c30f47f9c16f227ba82f0bdfd14fa968453c30b7677a07903b3b4f34b98d49"
name = "gopkg.in/square/go-jose.v2"
packages = [
".",
"cipher",
"json",
]
pruneopts = "UT"
revision = "628223f44a71f715d2881ea69afc795a1e9c01be"
version = "v2.3.0"
[[projects]]
digest = "1:2a81c6e126d36ad027328cffaa4888fc3be40f09dc48028d1f93705b718130b9"
name = "gopkg.in/yaml.v2"
@ -467,18 +570,108 @@
revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5"
version = "v2.1.1"
[[projects]]
digest = "1:7727a365529cdf6af394821dd990b046c56b8afac31e15e78fed58cf7bc179ad"
name = "k8s.io/api"
packages = [
"admissionregistration/v1alpha1",
"admissionregistration/v1beta1",
"apps/v1",
"apps/v1beta1",
"apps/v1beta2",
"authentication/v1",
"authentication/v1beta1",
"authorization/v1",
"authorization/v1beta1",
"autoscaling/v1",
"autoscaling/v2beta1",
"batch/v1",
"batch/v1beta1",
"batch/v2alpha1",
"certificates/v1beta1",
"core/v1",
"events/v1beta1",
"extensions/v1beta1",
"networking/v1",
"policy/v1beta1",
"rbac/v1",
"rbac/v1alpha1",
"rbac/v1beta1",
"scheduling/v1alpha1",
"scheduling/v1beta1",
"settings/v1alpha1",
"storage/v1",
"storage/v1alpha1",
"storage/v1beta1",
]
pruneopts = "UT"
revision = "5cb15d34447165a97c76ed5a60e4e99c8a01ecfe"
version = "kubernetes-1.13.4"
[[projects]]
branch = "master"
digest = "1:8dce42a5fac31cbfdc756bd244389280a905a5364b21dd44cdcb044ee622bf8b"
digest = "1:d0d43cf61b49d2750351759e1d220134ab7731db608b6716dc4ed792a493027d"
name = "k8s.io/apimachinery"
packages = ["pkg/version"]
packages = [
"pkg/api/errors",
"pkg/api/resource",
"pkg/apis/meta/v1",
"pkg/apis/meta/v1/unstructured",
"pkg/conversion",
"pkg/conversion/queryparams",
"pkg/fields",
"pkg/labels",
"pkg/runtime",
"pkg/runtime/schema",
"pkg/runtime/serializer",
"pkg/runtime/serializer/json",
"pkg/runtime/serializer/protobuf",
"pkg/runtime/serializer/recognizer",
"pkg/runtime/serializer/streaming",
"pkg/runtime/serializer/versioning",
"pkg/selection",
"pkg/types",
"pkg/util/clock",
"pkg/util/errors",
"pkg/util/framer",
"pkg/util/intstr",
"pkg/util/json",
"pkg/util/net",
"pkg/util/runtime",
"pkg/util/sets",
"pkg/util/validation",
"pkg/util/validation/field",
"pkg/util/wait",
"pkg/util/yaml",
"pkg/version",
"pkg/watch",
"third_party/forked/golang/reflect",
]
pruneopts = "UT"
revision = "f534d624797b270e5e46104dc7e2c2d61edbb85d"
[[projects]]
digest = "1:bf83d8940d59a175dad5ba323b47c6e62b3ba2dc9b2d449f629288deeeeb6582"
digest = "1:b2a0bdcfc59bed6a64d3ade946f9bf807f8fcd105892d940a008b0b2816babe5"
name = "k8s.io/client-go"
packages = ["util/homedir"]
packages = [
"kubernetes/scheme",
"kubernetes/typed/authentication/v1beta1",
"pkg/apis/clientauthentication",
"pkg/apis/clientauthentication/v1alpha1",
"pkg/apis/clientauthentication/v1beta1",
"pkg/version",
"plugin/pkg/client/auth/exec",
"rest",
"rest/watch",
"tools/clientcmd/api",
"tools/metrics",
"transport",
"util/cert",
"util/connrotation",
"util/flowcontrol",
"util/homedir",
"util/integer",
]
pruneopts = "UT"
revision = "7d04d0e2a0a1a4d4a1cd6baa432a2301492e4e65"
version = "v8.0.0"
@ -525,6 +718,7 @@
"github.com/casbin/casbin/model",
"github.com/casbin/casbin/persist",
"github.com/casbin/casbin/util",
"github.com/coreos/go-oidc",
"github.com/dghubble/sling",
"github.com/dgrijalva/jwt-go",
"github.com/docker/distribution",
@ -564,6 +758,10 @@
"golang.org/x/oauth2/clientcredentials",
"gopkg.in/ldap.v2",
"gopkg.in/yaml.v2",
"k8s.io/api/authentication/v1beta1",
"k8s.io/apimachinery/pkg/apis/meta/v1",
"k8s.io/client-go/kubernetes/typed/authentication/v1beta1",
"k8s.io/client-go/rest",
"k8s.io/helm/cmd/helm/search",
"k8s.io/helm/pkg/chartutil",
"k8s.io/helm/pkg/proto/hapi/chart",

View File

@ -112,6 +112,14 @@ ignored = ["github.com/goharbor/harbor/tests*"]
name = "github.com/robfig/cron"
version = "=1.0"
[[constraint]]
name = "github.com/coreos/go-oidc"
version = "=2.0.0"
[[constraint]]
name = "gopkg.in/yaml.v2"
version = "=2.1.1"
[[constraint]]
name = "k8s.io/api"
version = "kubernetes-1.13.4"

View File

@ -17,6 +17,7 @@ package config
import (
"fmt"
"os"
"sync"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/config/metadata"
@ -52,6 +53,7 @@ func NewRESTCfgManager(configURL, secret string) *CfgManager {
// InMemoryDriver driver for unit testing
type InMemoryDriver struct {
sync.Mutex
cfgMap map[string]interface{}
}
@ -59,11 +61,19 @@ type InMemoryDriver struct {
// it should be invoked before get any user scope config
// for system scope config, because it is immutable, no need to call this method
func (d *InMemoryDriver) Load() (map[string]interface{}, error) {
return d.cfgMap, nil
d.Lock()
defer d.Unlock()
res := make(map[string]interface{})
for k, v := range d.cfgMap {
res[k] = v
}
return res, nil
}
// Save only save user config setting to driver, for example: database, REST
func (d *InMemoryDriver) Save(cfg map[string]interface{}) error {
d.Lock()
defer d.Unlock()
for k, v := range cfg {
d.cfgMap[k] = v
}

View File

@ -132,6 +132,7 @@ var (
{Name: "uaa_verify_cert", Scope: UserScope, Group: UAAGroup, EnvKey: "UAA_VERIFY_CERT", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
{Name: common.HTTPAuthProxyEndpoint, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}},
{Name: common.HTTPAuthProxyTokenReviewEndpoint, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}},
{Name: common.HTTPAuthProxySkipCertVerify, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "false", ItemType: &BoolType{}},
{Name: common.HTTPAuthProxyAlwaysOnboard, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "false", ItemType: &BoolType{}},

View File

@ -61,11 +61,11 @@ type AuthModeType struct {
}
func (t *AuthModeType) validate(str string) error {
if str == common.LDAPAuth || str == common.DBAuth || str == common.UAAAuth || str == common.HTTPAuth {
if str == common.LDAPAuth || str == common.DBAuth || str == common.UAAAuth || str == common.HTTPAuth || str == common.OIDCAuth {
return nil
}
return fmt.Errorf("invalid %s, shoud be one of %s, %s, %s, %s",
common.AUTHMode, common.DBAuth, common.LDAPAuth, common.UAAAuth, common.HTTPAuth)
return fmt.Errorf("invalid %s, shoud be one of %s, %s, %s, %s, %s",
common.AUTHMode, common.DBAuth, common.LDAPAuth, common.UAAAuth, common.HTTPAuth, common.OIDCAuth)
}
// ProjectCreationRestrictionType ...

View File

@ -97,6 +97,7 @@ const (
UAAClientSecret = "uaa_client_secret"
UAAVerifyCert = "uaa_verify_cert"
HTTPAuthProxyEndpoint = "http_authproxy_endpoint"
HTTPAuthProxyTokenReviewEndpoint = "http_authproxy_tokenreview_endpoint"
HTTPAuthProxySkipCertVerify = "http_authproxy_skip_cert_verify"
HTTPAuthProxyAlwaysOnboard = "http_authproxy_always_onboard"
OIDCName = "oidc_name"
@ -129,8 +130,10 @@ const (
DefaultClairHealthCheckServerURL = "http://clair:6061"
// Use this prefix to distinguish harbor user, the prefix contains a special character($), so it cannot be registered as a harbor user.
RobotPrefix = "robot$"
// Use this prefix to index user who tries to login with web hook token.
AuthProxyUserNamePrefix = "tokenreview$"
CoreConfigPath = "/api/internal/configurations"
RobotTokenDuration = "robot_token_duration"
OIDCCallbackPath = "/c/oidc_callback"
OIDCCallbackPath = "/c/oidc/callback"
)

168
src/common/dao/oidc_user.go Normal file
View File

@ -0,0 +1,168 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"fmt"
"strings"
"time"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/pkg/errors"
)
var (
// ErrDupUser ...
ErrDupUser = errors.New("sql: duplicate user in harbor_user")
// ErrRollBackUser ...
ErrRollBackUser = errors.New("sql: transaction roll back error in harbor_user")
// ErrDupOIDCUser ...
ErrDupOIDCUser = errors.New("sql: duplicate user in oicd_user")
// ErrRollBackOIDCUser ...
ErrRollBackOIDCUser = errors.New("sql: transaction roll back error in oicd_user")
)
// GetOIDCUserByID ...
func GetOIDCUserByID(id int64) (*models.OIDCUser, error) {
oidcUser := &models.OIDCUser{
ID: id,
}
if err := GetOrmer().Read(oidcUser); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return oidcUser, nil
}
// GetUserBySubIss ...
func GetUserBySubIss(sub, issuer string) (*models.User, error) {
var oidcUsers []models.OIDCUser
n, err := GetOrmer().Raw(`select * from oidc_user where subiss = ? `, sub+issuer).QueryRows(&oidcUsers)
if err != nil {
return nil, err
}
if n == 0 {
return nil, nil
}
user, err := GetUser(models.User{
UserID: oidcUsers[0].UserID,
})
if err != nil {
return nil, err
}
if user == nil {
return nil, fmt.Errorf("can not get user %d", oidcUsers[0].UserID)
}
return user, nil
}
// GetOIDCUserByUserID ...
func GetOIDCUserByUserID(userID int) (*models.OIDCUser, error) {
var oidcUsers []models.OIDCUser
n, err := GetOrmer().Raw(`select * from oidc_user where user_id = ? `, userID).QueryRows(&oidcUsers)
if err != nil {
return nil, err
}
if n == 0 {
return nil, nil
}
return &oidcUsers[0], nil
}
// UpdateOIDCUser ...
func UpdateOIDCUser(oidcUser *models.OIDCUser) error {
oidcUser.UpdateTime = time.Now()
_, err := GetOrmer().Update(oidcUser)
return err
}
// DeleteOIDCUser ...
func DeleteOIDCUser(id int64) error {
_, err := GetOrmer().QueryTable(&models.OIDCUser{}).Filter("ID", id).Delete()
return err
}
// OnBoardOIDCUser onboard OIDC user
// For the api caller, should only care about the ErrDupUser. It could lead to http.StatusConflict.
func OnBoardOIDCUser(u *models.User) error {
if u.OIDCUserMeta == nil {
return errors.New("unable to onboard as empty oidc user")
}
o := orm.NewOrm()
err := o.Begin()
if err != nil {
return err
}
var errInsert error
// insert user
now := time.Now()
u.CreationTime = now
userID, err := o.Insert(u)
if err != nil {
errInsert = err
log.Errorf("fail to insert user, %v", err)
if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
errInsert = errors.Wrap(errInsert, ErrDupUser.Error())
}
err := o.Rollback()
if err != nil {
log.Errorf("fail to rollback when to onboard oidc user, %v", err)
errInsert = errors.Wrap(errInsert, err.Error())
return errors.Wrap(errInsert, ErrRollBackUser.Error())
}
return errInsert
}
u.UserID = int(userID)
u.OIDCUserMeta.UserID = int(userID)
// insert oidc user
now = time.Now()
u.OIDCUserMeta.CreationTime = now
_, err = o.Insert(u.OIDCUserMeta)
if err != nil {
errInsert = err
log.Errorf("fail to insert oidc user, %v", err)
if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
errInsert = errors.Wrap(errInsert, ErrDupOIDCUser.Error())
}
err := o.Rollback()
if err != nil {
errInsert = errors.Wrap(errInsert, err.Error())
return errors.Wrap(errInsert, ErrRollBackOIDCUser.Error())
}
return errInsert
}
err = o.Commit()
if err != nil {
log.Errorf("fail to commit when to onboard oidc user, %v", err)
return fmt.Errorf("fail to commit when to onboard oidc user, %v", err)
}
return nil
}

View File

@ -0,0 +1,186 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"testing"
"github.com/goharbor/harbor/src/common/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOIDCUserMetaDaoMethods(t *testing.T) {
user111 := models.User{
Username: "user111",
Email: "user111@email.com",
}
user222 := models.User{
Username: "user222",
Email: "user222@email.com",
}
userEmptyOuMeta := models.User{
Username: "userEmptyOuMeta",
Email: "userEmptyOuMeta@email.com",
}
ou111 := models.OIDCUser{
SubIss: "QWE123123RT1",
Secret: "QWEQWE1",
}
ou222 := models.OIDCUser{
SubIss: "QWE123123RT2",
Secret: "QWEQWE2",
}
// onboard OIDC ...
user111.OIDCUserMeta = &ou111
err := OnBoardOIDCUser(&user111)
require.Nil(t, err)
defer CleanUser(int64(user111.UserID))
user222.OIDCUserMeta = &ou222
err = OnBoardOIDCUser(&user222)
require.Nil(t, err)
defer CleanUser(int64(user222.UserID))
// empty OIDC user meta ...
err = OnBoardOIDCUser(&userEmptyOuMeta)
require.NotNil(t, err)
assert.Equal(t, "unable to onboard as empty oidc user", err.Error())
// test get by ID
oidcUser1, err := GetOIDCUserByID(ou111.ID)
require.Nil(t, err)
assert.Equal(t, ou111.UserID, oidcUser1.UserID)
// test get by userID
oidcUser2, err := GetOIDCUserByUserID(user111.UserID)
require.Nil(t, err)
assert.Equal(t, "QWE123123RT1", oidcUser2.SubIss)
// test get by sub and iss
userGetBySubIss, err := GetUserBySubIss("QWE123", "123RT1")
require.Nil(t, err)
assert.Equal(t, "user111@email.com", userGetBySubIss.Email)
// test update
meta3 := &models.OIDCUser{
ID: ou111.ID,
UserID: ou111.UserID,
SubIss: "newSub",
}
require.Nil(t, UpdateOIDCUser(meta3))
oidcUser1Update, err := GetOIDCUserByID(ou111.ID)
require.Nil(t, err)
assert.Equal(t, "newSub", oidcUser1Update.SubIss)
user, err := GetUserBySubIss("new", "Sub")
require.Nil(t, err)
assert.Equal(t, "user111", user.Username)
// clear data
defer func() {
_, err := GetOrmer().Raw(`delete from oidc_user`).Exec()
require.Nil(t, err)
}()
}
func TestOIDCOnboard(t *testing.T) {
user333 := models.User{
Username: "user333",
Email: "user333@email.com",
}
user555 := models.User{
Username: "user555",
Email: "user555@email.com",
}
user666 := models.User{
Username: "user666",
Email: "user666@email.com",
}
userDup := models.User{
Username: "user333",
Email: "userDup@email.com",
}
ou333 := &models.OIDCUser{
SubIss: "QWE123123RT3",
Secret: "QWEQWE333",
}
ou555 := &models.OIDCUser{
SubIss: "QWE123123RT5",
Secret: "QWEQWE555",
}
ouDup := &models.OIDCUser{
SubIss: "QWE123123RT3",
Secret: "QWEQWE333",
}
ouDupSub := &models.OIDCUser{
SubIss: "QWE123123RT3",
Secret: "ouDupSub",
}
// data prepare ...
user333.OIDCUserMeta = ou333
err := OnBoardOIDCUser(&user333)
require.Nil(t, err)
defer CleanUser(int64(user333.UserID))
// duplicate user -- ErrDupRows
// userDup is duplicate with user333
userDup.OIDCUserMeta = ou555
err = OnBoardOIDCUser(&userDup)
require.NotNil(t, err)
require.Contains(t, err.Error(), ErrDupUser.Error())
exist, err := UserExists(userDup, "email")
require.Nil(t, err)
require.False(t, exist)
// duplicate OIDC user -- ErrDupRows
// ouDup is duplicate with ou333
user555.OIDCUserMeta = ouDup
err = OnBoardOIDCUser(&user555)
require.NotNil(t, err)
require.Contains(t, err.Error(), ErrDupOIDCUser.Error())
exist, err = UserExists(user555, "username")
require.Nil(t, err)
require.False(t, exist)
// success
user555.OIDCUserMeta = ou555
err = OnBoardOIDCUser(&user555)
require.Nil(t, err)
exist, err = UserExists(user555, "username")
require.Nil(t, err)
require.True(t, exist)
defer CleanUser(int64(user555.UserID))
// duplicate OIDC user's sub -- ErrDupRows
// ouDup is duplicate with ou333
user666.OIDCUserMeta = ouDupSub
err = OnBoardOIDCUser(&user666)
require.NotNil(t, err)
require.Contains(t, err.Error(), ErrDupOIDCUser.Error())
exist, err = UserExists(user666, "username")
require.Nil(t, err)
require.False(t, exist)
// clear data
defer func() {
_, err := GetOrmer().Raw(`delete from oidc_user`).Exec()
require.Nil(t, err)
}()
}

View File

@ -38,5 +38,6 @@ func init() {
new(UserGroup),
new(AdminJob),
new(JobLog),
new(Robot))
new(Robot),
new(OIDCUser))
}

View File

@ -68,6 +68,7 @@ type Email struct {
// HTTPAuthProxy wraps the settings for HTTP auth proxy
type HTTPAuthProxy struct {
Endpoint string `json:"endpoint"`
TokenReviewEndpoint string `json:"tokenreivew_endpoint"`
SkipCertVerify bool `json:"skip_cert_verify"`
AlwaysOnBoard bool `json:"always_onboard"`
}

View File

@ -0,0 +1,20 @@
package models
import (
"time"
)
// OIDCUser ...
type OIDCUser struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
UserID int `orm:"column(user_id)" json:"user_id"`
Secret string `orm:"column(secret)" json:"secret"`
SubIss string `orm:"column(subiss)" json:"subiss"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
}
// TableName ...
func (o *OIDCUser) TableName() string {
return "oidc_user"
}

View File

@ -41,6 +41,7 @@ type User struct {
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
GroupList []*UserGroup `orm:"-" json:"-"`
OIDCUserMeta *OIDCUser `orm:"-" json:"oidc_user_meta,omitempty"`
}
// UserQuery ...

View File

@ -0,0 +1,188 @@
// Copyright 2018 Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package oidc
import (
"context"
"crypto/tls"
"errors"
"fmt"
gooidc "github.com/coreos/go-oidc"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"golang.org/x/oauth2"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
const googleEndpoint = "https://accounts.google.com"
type providerHelper struct {
sync.Mutex
ep endpoint
instance atomic.Value
setting atomic.Value
}
type endpoint struct {
url string
skipCertVerify bool
}
func (p *providerHelper) get() (*gooidc.Provider, error) {
if p.instance.Load() != nil {
s := p.setting.Load().(models.OIDCSetting)
if s.Endpoint != p.ep.url || s.SkipCertVerify != p.ep.skipCertVerify { // relevant settings have changed, need to re-create provider.
if err := p.create(); err != nil {
return nil, err
}
}
} else {
p.Lock()
defer p.Unlock()
if p.instance.Load() == nil {
if err := p.reload(); err != nil {
return nil, err
}
if err := p.create(); err != nil {
return nil, err
}
go func() {
for {
if err := p.reload(); err != nil {
log.Warningf("Failed to refresh configuration, error: %v", err)
}
time.Sleep(3 * time.Second)
}
}()
}
}
return p.instance.Load().(*gooidc.Provider), nil
}
func (p *providerHelper) reload() error {
conf, err := config.OIDCSetting()
if err != nil {
return fmt.Errorf("failed to load OIDC setting: %v", err)
}
p.setting.Store(*conf)
return nil
}
func (p *providerHelper) create() error {
if p.setting.Load() == nil {
return errors.New("the configuration is not loaded")
}
s := p.setting.Load().(models.OIDCSetting)
var client *http.Client
if s.SkipCertVerify {
client = &http.Client{
Transport: insecureTransport,
}
} else {
client = &http.Client{}
}
ctx := context.Background()
gooidc.ClientContext(ctx, client)
provider, err := gooidc.NewProvider(ctx, s.Endpoint)
if err != nil {
return fmt.Errorf("failed to create OIDC provider, error: %v", err)
}
p.instance.Store(provider)
p.ep = endpoint{
url: s.Endpoint,
skipCertVerify: s.SkipCertVerify,
}
return nil
}
var provider = &providerHelper{}
var insecureTransport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
// Token wraps the attributes of a oauth2 token plus the attribute of ID token
type Token struct {
*oauth2.Token
IDToken string `json:"id_token"`
}
func getOauthConf() (*oauth2.Config, error) {
p, err := provider.get()
if err != nil {
return nil, err
}
setting := provider.setting.Load().(models.OIDCSetting)
scopes := []string{}
for _, sc := range setting.Scope {
if strings.HasPrefix(p.Endpoint().AuthURL, googleEndpoint) && sc == gooidc.ScopeOfflineAccess {
log.Warningf("Dropped unsupported scope: %s ", sc)
continue
}
scopes = append(scopes, sc)
}
return &oauth2.Config{
ClientID: setting.ClientID,
ClientSecret: setting.ClientSecret,
Scopes: scopes,
RedirectURL: setting.RedirectURL,
Endpoint: p.Endpoint(),
}, nil
}
// AuthCodeURL returns the URL for OIDC provider's consent page. The state should be verified when user is redirected
// back to Harbor.
func AuthCodeURL(state string) (string, error) {
conf, err := getOauthConf()
if err != nil {
log.Errorf("Failed to get OAuth configuration, error: %v", err)
return "", err
}
if strings.HasPrefix(conf.Endpoint.AuthURL, googleEndpoint) {
return conf.AuthCodeURL(state, oauth2.AccessTypeOffline), nil
}
return conf.AuthCodeURL(state), nil
}
// ExchangeToken get the token from token provider via the code
func ExchangeToken(ctx context.Context, code string) (*Token, error) {
oauth, err := getOauthConf()
if err != nil {
log.Errorf("Failed to get OAuth configuration, error: %v", err)
return nil, err
}
oauthToken, err := oauth.Exchange(ctx, code)
if err != nil {
return nil, err
}
return &Token{Token: oauthToken, IDToken: oauthToken.Extra("id_token").(string)}, nil
}
// VerifyToken verifies the ID token based on the OIDC settings
func VerifyToken(ctx context.Context, rawIDToken string) (*gooidc.IDToken, error) {
p, err := provider.get()
if err != nil {
return nil, err
}
verifier := p.Verifier(&gooidc.Config{ClientID: provider.setting.Load().(models.OIDCSetting).ClientID})
return verifier.Verify(ctx, rawIDToken)
}

View File

@ -0,0 +1,98 @@
// Copyright 2018 Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package oidc
import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/core/config"
"github.com/stretchr/testify/assert"
"net/url"
"os"
"strings"
"testing"
"time"
)
func TestMain(m *testing.M) {
conf := map[string]interface{}{
common.OIDCName: "test",
common.OIDCEndpoint: "https://accounts.google.com",
common.OIDCSkipCertVerify: "false",
common.OIDCScope: "openid, profile, offline_access",
common.OIDCCLientID: "client",
common.OIDCClientSecret: "secret",
common.ExtEndpoint: "https://harbor.test",
}
config.InitWithSettings(conf)
result := m.Run()
if result != 0 {
os.Exit(result)
}
}
func TestHelperLoadConf(t *testing.T) {
testP := &providerHelper{}
assert.Nil(t, testP.setting.Load())
err := testP.reload()
assert.Nil(t, err)
assert.Equal(t, "test", testP.setting.Load().(models.OIDCSetting).Name)
assert.Equal(t, endpoint{}, testP.ep)
}
func TestHelperCreate(t *testing.T) {
testP := &providerHelper{}
err := testP.reload()
assert.Nil(t, err)
assert.Nil(t, testP.instance.Load())
err = testP.create()
assert.Nil(t, err)
assert.EqualValues(t, "https://accounts.google.com", testP.ep.url)
assert.NotNil(t, testP.instance.Load())
}
func TestHelperGet(t *testing.T) {
testP := &providerHelper{}
p, err := testP.get()
assert.Nil(t, err)
assert.Equal(t, "https://oauth2.googleapis.com/token", p.Endpoint().TokenURL)
update := map[string]interface{}{
common.OIDCName: "test",
common.OIDCEndpoint: "https://accounts.google.com",
common.OIDCSkipCertVerify: "false",
common.OIDCScope: "openid, profile, offline_access",
common.OIDCCLientID: "client",
common.OIDCClientSecret: "new-secret",
common.ExtEndpoint: "https://harbor.test",
}
config.GetCfgManager().UpdateConfig(update)
t.Log("Sleep for 5 seconds")
time.Sleep(5 * time.Second)
assert.Equal(t, "new-secret", testP.setting.Load().(models.OIDCSetting).ClientSecret)
}
func TestAuthCodeURL(t *testing.T) {
res, err := AuthCodeURL("random")
assert.Nil(t, err)
u, err := url.ParseRequestURI(res)
assert.Nil(t, err)
q, err := url.ParseQuery(u.RawQuery)
assert.Nil(t, err)
assert.Equal(t, "offline", q.Get("access_type"))
assert.False(t, strings.Contains(q.Get("scope"), "offline_access"))
}

View File

@ -234,3 +234,24 @@ func GetStrValueOfAnyType(value interface{}) string {
}
return strVal
}
// IsIllegalLength ...
func IsIllegalLength(s string, min int, max int) bool {
if min == -1 {
return (len(s) > max)
}
if max == -1 {
return (len(s) <= min)
}
return (len(s) < min || len(s) > max)
}
// IsContainIllegalChar ...
func IsContainIllegalChar(s string, illegalChar []string) bool {
for _, c := range illegalChar {
if strings.Index(s, c) >= 0 {
return true
}
}
return false
}

View File

@ -511,7 +511,7 @@ func (p *ProjectAPI) Logs() {
// TODO move this to pa ckage models
func validateProjectReq(req *models.ProjectRequest) error {
pn := req.Name
if isIllegalLength(req.Name, projectNameMinLen, projectNameMaxLen) {
if utils.IsIllegalLength(req.Name, projectNameMinLen, projectNameMaxLen) {
return fmt.Errorf("Project name is illegal in length. (greater than %d or less than %d)", projectNameMaxLen, projectNameMinLen)
}
validProjectName := regexp.MustCompile(`^` + restrictedNameChars + `$`)

View File

@ -16,11 +16,6 @@ package api
import (
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
@ -29,6 +24,9 @@ import (
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"net/http"
"regexp"
"strconv"
)
// UserAPI handles request to /api/users/{}
@ -446,13 +444,13 @@ func (ua *UserAPI) modifiable() bool {
// validate only validate when user register
func validate(user models.User) error {
if isIllegalLength(user.Username, 1, 255) {
if utils.IsIllegalLength(user.Username, 1, 255) {
return fmt.Errorf("username with illegal length")
}
if isContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) {
if utils.IsContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) {
return fmt.Errorf("username contains illegal characters")
}
if isIllegalLength(user.Password, 8, 20) {
if utils.IsIllegalLength(user.Password, 8, 20) {
return fmt.Errorf("password with illegal length")
}
return commonValidate(user)
@ -469,35 +467,16 @@ func commonValidate(user models.User) error {
return fmt.Errorf("Email can't be empty")
}
if isIllegalLength(user.Realname, 1, 255) {
if utils.IsIllegalLength(user.Realname, 1, 255) {
return fmt.Errorf("realname with illegal length")
}
if isContainIllegalChar(user.Realname, []string{",", "~", "#", "$", "%"}) {
if utils.IsContainIllegalChar(user.Realname, []string{",", "~", "#", "$", "%"}) {
return fmt.Errorf("realname contains illegal characters")
}
if isIllegalLength(user.Comment, -1, 30) {
if utils.IsIllegalLength(user.Comment, -1, 30) {
return fmt.Errorf("comment with illegal length")
}
return nil
}
func isIllegalLength(s string, min int, max int) bool {
if min == -1 {
return (len(s) > max)
}
if max == -1 {
return (len(s) <= min)
}
return (len(s) < min || len(s) > max)
}
func isContainIllegalChar(s string, illegalChar []string) bool {
for _, c := range illegalChar {
if strings.Index(s, c) >= 0 {
return true
}
}
return false
}

View File

@ -472,6 +472,7 @@ func HTTPAuthProxySetting() (*models.HTTPAuthProxy, error) {
}
return &models.HTTPAuthProxy{
Endpoint: cfgMgr.Get(common.HTTPAuthProxyEndpoint).GetString(),
TokenReviewEndpoint: cfgMgr.Get(common.HTTPAuthProxyTokenReviewEndpoint).GetString(),
SkipCertVerify: cfgMgr.Get(common.HTTPAuthProxySkipCertVerify).GetBool(),
AlwaysOnBoard: cfgMgr.Get(common.HTTPAuthProxyAlwaysOnboard).GetBool(),
}, nil

View File

@ -260,6 +260,6 @@ func TestOIDCSetting(t *testing.T) {
assert.True(t, v.SkipCertVerify)
assert.Equal(t, "client", v.ClientID)
assert.Equal(t, "secret", v.ClientSecret)
assert.Equal(t, "https://harbor.test/c/oidc_callback", v.RedirectURL)
assert.Equal(t, "https://harbor.test/c/oidc/callback", v.RedirectURL)
assert.ElementsMatch(t, []string{"openid", "profile"}, v.Scope)
}

View File

@ -138,4 +138,5 @@ func TestAll(t *testing.T) {
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(404), w.Code, "GET v2/noproject/manifests/1.0 should get a 404 response")
}

View File

@ -0,0 +1,150 @@
// Copyright 2018 Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/oidc"
"github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/core/config"
"github.com/pkg/errors"
"net/http"
"strings"
)
const idTokenKey = "oidc_id_token"
const stateKey = "oidc_state"
// OIDCController handles requests for OIDC login, callback and user onboard
type OIDCController struct {
api.BaseController
}
type oidcUserData struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Username string `json:"name"`
Email string `json:"email"`
}
// Prepare include public code path for call request handler of OIDCController
func (oc *OIDCController) Prepare() {
if mode, _ := config.AuthMode(); mode != common.OIDCAuth {
oc.CustomAbort(http.StatusPreconditionFailed, fmt.Sprintf("Auth Mode: %s is not OIDC based.", mode))
}
}
// RedirectLogin redirect user's browser to OIDC provider's login page
func (oc *OIDCController) RedirectLogin() {
state := utils.GenerateRandomString()
url, err := oidc.AuthCodeURL(state)
if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err)
return
}
oc.SetSession(stateKey, state)
// Force to use the func 'Redirect' of beego.Controller
oc.Controller.Redirect(url, http.StatusFound)
}
// Callback handles redirection from OIDC provider. It will exchange the token and
// kick off onboard if needed.
func (oc *OIDCController) Callback() {
if oc.Ctx.Request.URL.Query().Get("state") != oc.GetSession(stateKey) {
oc.RenderError(http.StatusBadRequest, "State mismatch.")
return
}
code := oc.Ctx.Request.URL.Query().Get("code")
ctx := oc.Ctx.Request.Context()
token, err := oidc.ExchangeToken(ctx, code)
if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err)
return
}
idToken, err := oidc.VerifyToken(ctx, token.IDToken)
if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err)
return
}
d := &oidcUserData{}
err = idToken.Claims(d)
if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err)
return
}
ouDataStr, err := json.Marshal(d)
if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err)
return
}
oc.SetSession(idTokenKey, string(ouDataStr))
// TODO: check and trigger onboard popup or redirect user to project page
oc.Data["json"] = d
oc.ServeFormatted()
}
// Onboard handles the request to onboard an user authenticated via OIDC provider
func (oc *OIDCController) Onboard() {
username := oc.GetString("username")
if utils.IsIllegalLength(username, 1, 255) {
oc.RenderFormatedError(http.StatusBadRequest, errors.New("username with illegal length"))
return
}
if utils.IsContainIllegalChar(username, []string{",", "~", "#", "$", "%"}) {
oc.RenderFormatedError(http.StatusBadRequest, errors.New("username contains illegal characters"))
return
}
idTokenStr := oc.GetSession(idTokenKey)
d := &oidcUserData{}
err := json.Unmarshal([]byte(idTokenStr.(string)), &d)
if err != nil {
oc.RenderFormatedError(http.StatusInternalServerError, err)
return
}
oidcUser := models.OIDCUser{
SubIss: d.Subject + d.Issuer,
// TODO: get secret with secret manager.
Secret: utils.GenerateRandomString(),
}
var email string
if d.Email == "" {
email = utils.GenerateRandomString() + "@harbor.com"
}
user := models.User{
Username: username,
Email: email,
OIDCUserMeta: &oidcUser,
}
err = dao.OnBoardOIDCUser(&user)
if err != nil {
if strings.Contains(err.Error(), dao.ErrDupUser.Error()) {
oc.RenderFormatedError(http.StatusConflict, err)
return
}
oc.RenderFormatedError(http.StatusInternalServerError, err)
return
}
oc.Controller.Redirect(config.GetPortalURL(), http.StatusMovedPermanently)
}

View File

@ -39,6 +39,16 @@ import (
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/admiral"
"strings"
"encoding/json"
k8s_api_v1beta1 "k8s.io/api/authentication/v1beta1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
// ContextValueKey for content value
@ -100,6 +110,7 @@ func Init() {
// standalone
reqCtxModifiers = []ReqCtxModifier{
&secretReqCtxModifier{config.SecretStore},
&authProxyReqCtxModifier{},
&robotAuthReqCtxModifier{},
&basicAuthReqCtxModifier{},
&sessionReqCtxModifier{},
@ -194,6 +205,123 @@ func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
return true
}
type authProxyReqCtxModifier struct{}
func (ap *authProxyReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
authMode, err := config.AuthMode()
if err != nil {
log.Errorf("fail to get auth mode, %v", err)
return false
}
if authMode != common.HTTPAuth {
return false
}
// only support docker login
if ctx.Request.URL.Path != "/service/token" {
log.Debug("Auth proxy modifier only handles docker login request.")
return false
}
proxyUserName, proxyPwd, ok := ctx.Request.BasicAuth()
if !ok {
return false
}
rawUserName, match := ap.matchAuthProxyUserName(proxyUserName)
if !match {
log.Errorf("User name %s doesn't meet the auth proxy name pattern", proxyUserName)
return false
}
httpAuthProxyConf, err := config.HTTPAuthProxySetting()
if err != nil {
log.Errorf("fail to get auth proxy settings, %v", err)
return false
}
// Init auth client with the auth proxy endpoint.
authClientCfg := &rest.Config{
Host: httpAuthProxyConf.TokenReviewEndpoint,
ContentConfig: rest.ContentConfig{
GroupVersion: &schema.GroupVersion{},
NegotiatedSerializer: serializer.DirectCodecFactory{CodecFactory: scheme.Codecs},
},
BearerToken: proxyPwd,
TLSClientConfig: rest.TLSClientConfig{
Insecure: httpAuthProxyConf.SkipCertVerify,
},
}
authClient, err := rest.RESTClientFor(authClientCfg)
if err != nil {
log.Errorf("fail to create auth client, %v", err)
return false
}
// Do auth with the token.
tokenReviewRequest := &k8s_api_v1beta1.TokenReview{
TypeMeta: metav1.TypeMeta{
Kind: "TokenReview",
APIVersion: "authentication.k8s.io/v1beta1",
},
Spec: k8s_api_v1beta1.TokenReviewSpec{
Token: proxyPwd,
},
}
res := authClient.Post().Body(tokenReviewRequest).Do()
err = res.Error()
if err != nil {
log.Errorf("fail to POST auth request, %v", err)
return false
}
resRaw, err := res.Raw()
if err != nil {
log.Errorf("fail to get raw data of token review, %v", err)
return false
}
// Parse the auth response, check the user name and authenticated status.
tokenReviewResponse := &k8s_api_v1beta1.TokenReview{}
err = json.Unmarshal(resRaw, &tokenReviewResponse)
if err != nil {
log.Errorf("fail to decode token review, %v", err)
return false
}
if !tokenReviewResponse.Status.Authenticated {
log.Errorf("fail to auth user: %s", rawUserName)
return false
}
user, err := dao.GetUser(models.User{
Username: rawUserName,
})
if err != nil {
log.Errorf("fail to get user: %v", err)
return false
}
if user == nil {
log.Errorf("User: %s has not been on boarded yet.", rawUserName)
return false
}
if rawUserName != tokenReviewResponse.Status.User.Username {
log.Errorf("user name doesn't match with token: %s", rawUserName)
return false
}
log.Debug("using local database project manager")
pm := config.GlobalProjectMgr
log.Debug("creating local database security context for auth proxy...")
securCtx := local.NewSecurityContext(user, pm)
setSecurCtxAndPM(ctx.Request, securCtx, pm)
return true
}
func (ap *authProxyReqCtxModifier) matchAuthProxyUserName(name string) (string, bool) {
if !strings.HasPrefix(name, common.AuthProxyUserNamePrefix) {
return "", false
}
return strings.Replace(name, common.AuthProxyUserNamePrefix, "", -1), true
}
type basicAuthReqCtxModifier struct{}
func (b *basicAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {

View File

@ -28,6 +28,7 @@ import (
"github.com/astaxie/beego"
beegoctx "github.com/astaxie/beego/context"
"github.com/astaxie/beego/session"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
commonsecret "github.com/goharbor/harbor/src/common/secret"
"github.com/goharbor/harbor/src/common/security"
@ -40,6 +41,9 @@ import (
"github.com/goharbor/harbor/src/core/promgr"
driver_local "github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
"github.com/stretchr/testify/assert"
"github.com/goharbor/harbor/src/common"
fiter_test "github.com/goharbor/harbor/src/core/filter/test"
)
func TestMain(m *testing.M) {
@ -131,6 +135,67 @@ func TestRobotReqCtxModifier(t *testing.T) {
assert.False(t, modified)
}
func TestAutoProxyReqCtxModifier(t *testing.T) {
server, err := fiter_test.NewAuthProxyTestServer()
assert.Nil(t, err)
defer server.Close()
c := map[string]interface{}{
common.HTTPAuthProxyAlwaysOnboard: "true",
common.HTTPAuthProxySkipCertVerify: "true",
common.HTTPAuthProxyEndpoint: "https://auth.proxy/suffix",
common.HTTPAuthProxyTokenReviewEndpoint: server.URL,
common.AUTHMode: common.HTTPAuth,
}
config.Upload(c)
v, e := config.HTTPAuthProxySetting()
assert.Nil(t, e)
assert.Equal(t, *v, models.HTTPAuthProxy{
Endpoint: "https://auth.proxy/suffix",
AlwaysOnBoard: true,
SkipCertVerify: true,
TokenReviewEndpoint: server.URL,
})
// No onboard
req, err := http.NewRequest(http.MethodGet,
"http://127.0.0.1/service/token", nil)
if err != nil {
t.Fatalf("failed to create request: %v", req)
}
req.SetBasicAuth("tokenreview$administrator@vsphere.local", "reviEwt0k3n")
ctx, err := newContext(req)
if err != nil {
t.Fatalf("failed to crate context: %v", err)
}
modifier := &authProxyReqCtxModifier{}
modified := modifier.Modify(ctx)
assert.False(t, modified)
// Onboard
err = dao.OnBoardUser(&models.User{
Username: "administrator@vsphere.local",
})
assert.Nil(t, err)
req, err = http.NewRequest(http.MethodGet,
"http://127.0.0.1/service/token", nil)
if err != nil {
t.Fatalf("failed to create request: %v", req)
}
req.SetBasicAuth("tokenreview$administrator@vsphere.local", "reviEwt0k3n")
ctx, err = newContext(req)
if err != nil {
t.Fatalf("failed to crate context: %v", err)
}
modifier = &authProxyReqCtxModifier{}
modified = modifier.Modify(ctx)
assert.True(t, modified)
}
func TestBasicAuthReqCtxModifier(t *testing.T) {
req, err := http.NewRequest(http.MethodGet,
"http://127.0.0.1/api/projects/", nil)

View File

@ -0,0 +1,89 @@
package test
import (
"encoding/json"
"fmt"
"io/ioutil"
"k8s.io/api/authentication/v1beta1"
"net/http"
"net/http/httptest"
"net/url"
)
// NewAuthProxyTestServer mocks a https server for auth proxy.
func NewAuthProxyTestServer() (*httptest.Server, error) {
const webhookPath = "/authproxy/tokenreview"
serveHTTP := func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, fmt.Sprintf("unexpected method: %v", r.Method), http.StatusMethodNotAllowed)
return
}
if r.URL.Path != webhookPath {
http.Error(w, fmt.Sprintf("unexpected path: %v", r.URL.Path), http.StatusNotFound)
return
}
var review v1beta1.TokenReview
bodyData, _ := ioutil.ReadAll(r.Body)
if err := json.Unmarshal(bodyData, &review); err != nil {
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
return
}
// ensure we received the serialized tokenreview as expected
if review.APIVersion != "authentication.k8s.io/v1beta1" {
http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest)
return
}
type userInfo struct {
Username string `json:"username"`
UID string `json:"uid"`
Groups []string `json:"groups"`
Extra map[string][]string `json:"extra"`
}
type status struct {
Authenticated bool `json:"authenticated"`
User userInfo `json:"user"`
Audiences []string `json:"audiences"`
}
var extra map[string][]string
if review.Status.User.Extra != nil {
extra = map[string][]string{}
for k, v := range review.Status.User.Extra {
extra[k] = v
}
}
resp := struct {
Kind string `json:"kind"`
APIVersion string `json:"apiVersion"`
Status status `json:"status"`
}{
Kind: "TokenReview",
APIVersion: v1beta1.SchemeGroupVersion.String(),
Status: status{
true,
userInfo{
Username: "administrator@vsphere.local",
UID: review.Status.User.UID,
Groups: review.Status.User.Groups,
Extra: extra,
},
review.Status.Audiences,
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP))
server.StartTLS()
serverURL, _ := url.Parse(server.URL)
serverURL.Path = webhookPath
server.URL = serverURL.String()
return server, nil
}

View File

@ -15,6 +15,7 @@
package main
import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/controllers"
@ -37,6 +38,9 @@ func initRouters() {
beego.Router("/c/reset", &controllers.CommonController{}, "post:ResetPassword")
beego.Router("/c/userExists", &controllers.CommonController{}, "post:UserExists")
beego.Router("/c/sendEmail", &controllers.CommonController{}, "get:SendResetEmail")
beego.Router("/c/oidc/login", &controllers.OIDCController{}, "get:RedirectLogin")
beego.Router("/c/oidc/onboard", &controllers.OIDCController{}, "post:Onboard")
beego.Router(common.OIDCCallbackPath, &controllers.OIDCController{}, "get:Callback")
// API:
beego.Router("/api/projects/:pid([0-9]+)/members/?:pmid([0-9]+)", &api.ProjectMemberAPI{})

View File

@ -25,6 +25,8 @@ fdescribe('harbor-portal app', function () {
it('should display: ' + expectedMsg, () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual(expectedMsg)
page.getParagraphText().then(res => {
expect(res).toEqual(expectedMsg);
});
});
});

View File

@ -91,7 +91,12 @@ export class Configuration {
http_authproxy_endpoint?: StringValueItem;
http_authproxy_skip_cert_verify?: BoolValueItem;
http_authproxy_always_onboard?: BoolValueItem;
oidc_name?: StringValueItem;
oidc_endpoint?: StringValueItem;
oidc_client_id?: StringValueItem;
oidc_client_secret?: StringValueItem;
oidc_skip_cert_verify?: BoolValueItem;
oidc_scope?: StringValueItem;
public constructor() {
this.auth_mode = new StringValueItem("db_auth", true);
this.project_creation_restriction = new StringValueItem("everyone", true);
@ -136,5 +141,11 @@ export class Configuration {
this.http_authproxy_endpoint = new StringValueItem("", true);
this.http_authproxy_skip_cert_verify = new BoolValueItem(false, true);
this.http_authproxy_always_onboard = new BoolValueItem(false, true);
this.oidc_name = new StringValueItem('', true);
this.oidc_endpoint = new StringValueItem('', true);
this.oidc_client_id = new StringValueItem('', true);
this.oidc_client_secret = new StringValueItem('', true);
this.oidc_skip_cert_verify = new BoolValueItem(false, true);
this.oidc_scope = new StringValueItem('', true);
}
}

View File

@ -0,0 +1,20 @@
<h5 class="history-header" id="history-header">{{'GC.JOB_HISTORY' | translate}}</h5>
<clr-datagrid>
<clr-dg-column>{{'GC.JOB_ID' | translate}}</clr-dg-column>
<clr-dg-column>{{'GC.TRIGGER_TYPE' | translate}}</clr-dg-column>
<clr-dg-column>{{'STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'START_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'UPDATE_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'LOGS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let job of jobs" [clrDgItem]='job'>
<clr-dg-cell>{{job.id }}</clr-dg-cell>
<clr-dg-cell>{{(job.type ? 'SCHEDULE.'+ job.type.toUpperCase() : '') | translate }}</clr-dg-cell>
<clr-dg-cell>{{job.status.toUpperCase() | translate}}</clr-dg-cell>
<clr-dg-cell>{{job.createTime | date:'medium'}}</clr-dg-cell>
<clr-dg-cell>{{job.updateTime | date:'medium'}}</clr-dg-cell>
<clr-dg-cell>
<a *ngIf="job.status.toLowerCase() === 'finished' || job.status.toLowerCase() === 'error'" target="_blank" href="/api/system/gc/{{job.id}}/log"><clr-icon shape="list"></clr-icon></a>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{'GC.LATEST_JOBS' | translate :{param: jobs.length} }}</clr-dg-footer>
</clr-datagrid>

View File

@ -0,0 +1,4 @@
.history-header {
color: #000;
margin:20px 0 6px 0;
}

View File

@ -0,0 +1,28 @@
import { Component, OnInit } from '@angular/core';
import { GcRepoService } from "../gc.service";
import { GcJobViewModel } from "../gcLog";
import { GcViewModelFactory } from "../gc.viewmodel.factory";
@Component({
selector: 'gc-history',
templateUrl: './gc-history.component.html',
styleUrls: ['./gc-history.component.scss']
})
export class GcHistoryComponent implements OnInit {
jobs: Array<GcJobViewModel> = [];
constructor(
private gcRepoService: GcRepoService,
private gcViewModelFactory: GcViewModelFactory,
) { }
ngOnInit() {
this.getJobs();
}
getJobs() {
this.gcRepoService.getJobs().subscribe(jobs => {
this.jobs = this.gcViewModelFactory.createJobViewModel(jobs);
});
}
}

View File

@ -1,24 +1,4 @@
<div class="cron-selection">
<cron-selection [labelCurrent]="getLabelCurrent" [labelEdit]='getText' [originCron]='originCron' (inputvalue)="scheduleGc($event)"></cron-selection>
<button class="btn btn-outline btn-sm gc-start-btn" (click)="gcNow()" [disabled]="disableGC">{{'GC.GC_NOW' | translate}}</button>
</div>
<button class="btn btn-primary btn-sm gc-start-btn" (click)="gcNow()" [disabled]="disableGC">{{'GC.GC_NOW' | translate}}</button>
<div class="job-header font-style">{{'GC.JOB_HISTORY' | translate}}</div>
<clr-datagrid>
<clr-dg-column>{{'GC.JOB_ID' | translate}}</clr-dg-column>
<clr-dg-column>{{'GC.TRIGGER_TYPE' | translate}}</clr-dg-column>
<clr-dg-column>{{'STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'START_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'UPDATE_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'LOGS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let job of jobs" [clrDgItem]='job'>
<clr-dg-cell>{{job.id }}</clr-dg-cell>
<clr-dg-cell>{{(job.type ? 'SCHEDULE.'+ job.type.toUpperCase() : '') | translate }}</clr-dg-cell>
<clr-dg-cell>{{job.status.toUpperCase() | translate}}</clr-dg-cell>
<clr-dg-cell>{{job.createTime | date:'medium'}}</clr-dg-cell>
<clr-dg-cell>{{job.updateTime | date:'medium'}}</clr-dg-cell>
<clr-dg-cell>
<a *ngIf="job.status.toLowerCase() === 'finished' || job.status.toLowerCase() === 'error'" target="_blank" href="/api/system/gc/{{job.id}}/log"><clr-icon shape="list"></clr-icon></a>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{'GC.LATEST_JOBS' | translate :{param: jobs.length} }}</clr-dg-footer>
</clr-datagrid>

View File

@ -1,45 +1,10 @@
.flex-layout {
display: flex;
align-items: center;
margin:20px 0;
font-size: .541667rem;
}
.font-style {
color: #000;
font-size: .541667rem;
}
.cron-selection {
margin-top: 20px;
}
.setting-wrapper {
label {
width: 228px;
}
*:not(:first-child), span {
margin-right: 18px;
}
}
.normal-wrapper {
> span:first-child {
width: 228px;
}
> span:not(:first-child){
margin-right: 18px;
}
display: flex;
align-items: center;
}
.gc-start-btn {
width:150px;
}
.job-header {
margin:20px 0 -10px 0;
}
.day-selector-wrapper {
display: flex;
margin-top: 47px;
}

View File

@ -4,3 +4,5 @@ export * from "./gc.api.repository";
export * from "./gc.service";
export * from "./gc.viewmodel.factory";
export * from "./gcLog";
export * from "./gc-history/gc-history.component";

View File

@ -5,7 +5,7 @@ import { SystemSettingsComponent } from './system/system-settings.component';
import { VulnerabilityConfigComponent } from './vulnerability/vulnerability-config.component';
import { RegistryConfigComponent } from './registry-config.component';
import { GcComponent } from './gc/gc.component';
import { GcHistoryComponent } from './gc/gc-history/gc-history.component';
export * from './config';
export * from './replication/replication-config.component';
@ -16,6 +16,7 @@ export * from './gc/index';
export const CONFIGURATION_DIRECTIVES: Type<any>[] = [
ReplicationConfigComponent,
GcHistoryComponent,
GcComponent,
SystemSettingsComponent,
VulnerabilityConfigComponent,

View File

@ -17,6 +17,7 @@
<button id="config-gc" clrTabLink>{{'CONFIG.GC' | translate}}</button>
<clr-tab-content id="gc" *clrIfActive>
<gc-config #gcConfig></gc-config>
<gc-history></gc-history>
</clr-tab-content>
</clr-tab>
</clr-tabs>

View File

@ -9,6 +9,7 @@ import { VulnerabilityConfigComponent } from './vulnerability/vulnerability-conf
import { RegistryConfigComponent } from './registry-config.component';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { GcComponent } from './gc/gc.component';
import { GcHistoryComponent } from './gc/gc-history/gc-history.component';
import { CronScheduleComponent } from '../cron-schedule/cron-schedule.component';
import {
@ -67,6 +68,7 @@ describe('RegistryConfigComponent (inline template)', () => {
RegistryConfigComponent,
ConfirmationDialogComponent,
GcComponent,
GcHistoryComponent,
CronScheduleComponent
],
providers: [

View File

@ -1,8 +1,8 @@
<form #systemConfigFrom="ngForm" class="compact">
<section class="form-block">
<label class="section-title" *ngIf="showSubTitle">{{ 'CONFIG.SCANNING.TITLE' | translate }}</label>
<div class="form-group">
<label>{{ 'CONFIG.SCANNING.DB_REFRESH_TIME' | translate }}</label>
<div>
<label class="update-time">{{ 'CONFIG.SCANNING.DB_REFRESH_TIME' | translate }}</label>
<clr-tooltip *ngIf="!isClairDBFullyReady">
<clr-icon shape="warning" class="is-warning" size="22"></clr-icon>
<clr-tooltip-content [clrPosition]="'top-right'" [clrSize]="'md'" *clrIfOpen>
@ -23,9 +23,11 @@
</clr-dropdown>
<span *ngIf="isClairDBFullyReady && !showScanningNamespaces">{{ updatedTimestamp | date:'MM/dd/y HH:mm:ss' }} AM</span>
</div>
<div class="button-group">
<cron-selection [labelCurrent]="getLabelCurrent" [labelEdit]='getLabelCurrent' [originCron]='originCron' (inputvalue)="scanAll($event)"></cron-selection>
<div class="btn-scan-right btn-scan">
<button class="btn btn-primary btn-sm btn-scan" (click)="scanNow()" [disabled]="!scanAvailable">{{ 'CONFIG.SCANNING.SCAN_NOW' | translate }}</button><br>
<button class="btn btn-outline btn-sm btn-scan" (click)="scanNow()" [disabled]="!scanAvailable">{{ 'CONFIG.SCANNING.SCAN_NOW' | translate }}</button><br>
</div>
</div>
</section>
</form>

View File

@ -31,12 +31,20 @@
}
}
}
.btn-scan-right button{
.button-group {
display: flex;
align-items: center;
.btn-scan-right {
button{
width: 160px;
margin-bottom: 0px;
margin-top: 5px;
margin-top: 41px;
}
.btn-scan-right span{
margin-top: 4px;
}
}
.update-time {
color: #000;
font-size: .541667rem;
width: 200px;
}

View File

@ -1,4 +1,5 @@
<div class="normal-wrapper flex-layout" *ngIf="!isEditMode">
<div>
<span class="font-style">{{ labelCurrent | translate }}</span>
<span>{{(originScheduleType ? 'SCHEDULE.'+ originScheduleType.toUpperCase(): "") | translate}}</span>
<a [hidden]="originScheduleType!==SCHEDULE_TYPE.HOURLY" href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
@ -15,7 +16,8 @@
</a>
<span [hidden]="originScheduleType!==SCHEDULE_TYPE.CUSTOM">{{ "SCHEDULE.CRON" | translate }} :</span>
<span [hidden]="originScheduleType!==SCHEDULE_TYPE.CUSTOM">{{ oriCron }}</span>
<button class="btn btn-outline btn-sm" (click)="editSchedule()" id="editSchedule">
</div>
<button class="btn btn-primary btn-sm" (click)="editSchedule()" id="editSchedule">
{{ "BUTTON.EDIT" | translate }}
</button>
</div>
@ -31,15 +33,16 @@
</select>
</div>
<span [hidden]="scheduleType!==SCHEDULE_TYPE.CUSTOM">{{ "SCHEDULE.CRON" | translate }} :</span>
<div class="form-group" [hidden]="scheduleType!==SCHEDULE_TYPE.CUSTOM">
<label for="targetCron" aria-haspopup="true" role="tooltip" [class.invalid]="cronStringInput.invalid && (cronStringInput.dirty || cronStringInput.touched)" class="tooltip tooltip-validation tooltip-md tooltip-top-right cron-label">
<input type="text" name=targetCron id="targetCron" #cronStringInput="ngModel" required class="form-control"
<div [hidden]="scheduleType!==SCHEDULE_TYPE.CUSTOM">
<label for="targetCron" aria-haspopup="true" role="tooltip" [class.invalid]="dateInvalid" class="tooltip tooltip-validation tooltip-md tooltip-top-right cron-label">
<input type="text" (blur)="blurInvalid()" (input)="inputInvalid()" name=targetCron id="targetCron" #cronStringInput="ngModel" required class="form-control"
[(ngModel)]="cronString">
<span class="tooltip-content">
<span class="tooltip-content" *ngIf="dateInvalid">
{{'TOOLTIP.CRON_REQUIRED' | translate }}
</span>
</label>
</div>
<div class="confirm-button">
<button class="btn btn-primary btn-sm"
(click)="save()" id="config-save">
{{ "BUTTON.SAVE" | translate }}
@ -48,3 +51,4 @@
{{ "BUTTON.CANCEL" | translate }}
</button>
</div>
</div>

View File

@ -1,13 +1,11 @@
.flex-layout {
display: flex;
align-items: center;
margin: 20px 0;
font-size: .541667rem;
}
.normal-wrapper {
> span:first-child {
width: 228px;
width: 200px;
}
> span:not(:first-child) {
@ -17,13 +15,17 @@
margin-left: -10px;
}
button {
margin-left: 10px;
margin: 20px 20px 0 200px;
}
}
.setting-wrapper {
> span:first-child {
width: 228px;
width: 200px;
}
.confirm-button {
margin: 20px 0px 0 200px;
}
*:not(:first-child) {
@ -39,7 +41,10 @@
width: 195px;
}
}
.font-style {
display: inline-block;
color: #000;
font-size: .541667rem;
width: 200px;
}

View File

@ -8,6 +8,7 @@ import {
SimpleChange
} from "@angular/core";
import { OriginCron } from "../service/interface";
import { cronRegex } from "../utils";
const SCHEDULE_TYPE = {
NONE: "None",
DAILY: "Daily",
@ -24,6 +25,7 @@ export class CronScheduleComponent implements OnChanges {
@Input() originCron: OriginCron;
@Input() labelEdit: string;
@Input() labelCurrent: string;
dateInvalid: boolean;
originScheduleType: string;
oriCron: string;
cronString: string;
@ -52,13 +54,26 @@ export class CronScheduleComponent implements OnChanges {
}
}
inputInvalid() {
this.dateInvalid = cronRegex(this.cronString) ? false : true;
}
blurInvalid() {
if (!this.cronString) {
this.dateInvalid = true;
}
}
public resetSchedule() {
this.originScheduleType = this.scheduleType;
this.oriCron = this.cronString;
this.oriCron = this.cronString.replace(/\s+/g, " ").trim();
this.isEditMode = false;
}
save(): void {
if (this.dateInvalid && this.scheduleType === SCHEDULE_TYPE.CUSTOM) {
return;
}
let scheduleTerm: string = "";
this.resetSchedule();
if (this.scheduleType && this.scheduleType === SCHEDULE_TYPE.NONE) {

View File

@ -1,10 +1,11 @@
import { Observable } from "rxjs";
import { Http } from "@angular/http";
import { Injectable } from "@angular/core";
import { Injectable, Inject } from "@angular/core";
import { RetagRequest } from "./interface";
import { HTTP_JSON_OPTIONS } from "../utils";
import { catchError } from "rxjs/operators";
import { throwError as observableThrowError } from "rxjs/index";
import { IServiceConfig, SERVICE_CONFIG } from "../service.config";
/**
* Define the service methods to perform images retag.
@ -36,14 +37,16 @@ export abstract class RetagService {
@Injectable()
export class RetagDefaultService extends RetagService {
constructor(
private http: Http
private http: Http,
@Inject(SERVICE_CONFIG) private config: IServiceConfig
) {
super();
}
retag(request: RetagRequest): Observable<any> {
let baseUrl: string = this.config.repositoryBaseEndpoint ? this.config.repositoryBaseEndpoint : '/api/repositories';
return this.http
.post(`/api/repositories/${request.targetProject}/${request.targetRepo}/tags`,
.post(`${baseUrl}/${request.targetProject}/${request.targetRepo}/tags`,
{
"tag": request.targetTag,
"src_image": request.srcImage,

View File

@ -343,3 +343,24 @@ export function getChanges(original: any, afterChange: any): { [key: string]: an
}
return changes;
}
export function cronRegex(testValue: any): boolean {
const regSecond = "^($|#|\\w+\\s*=|(\\?|\\*|(?:[0-5]?\\d)(?:(?:-|\/|\\,)(?:[0-5]?\\d))?" +
"(?:,(?:[0-5]?\\d)(?:(?:-|\/|\\,)(?:[0-5]?\\d))?)*)\\s+";
const regMinute = "(\\?|\\*|(?:[0-5]?\\d)(?:(?:-|\/|\\,)(?:[0-5]?\\d))?(?:,(?:[0-5]?\\d)(?:(?:-|\/|\\,)(?:[0-5]?\\d))?)*)\\s+";
const regHour = "(\\?|\\*|(?:[01]?\\d|2[0-3])(?:(?:-|\/|\\,)(?:[01]?\\d|2[0-3]))?(?:,(?:[01]?\\d|2[0-3])" +
"(?:(?:-|\/|\\,)(?:[01]?\\d|2[0-3]))?)*)\\s+";
const regDay = "(\\?|\\*|(?:0?[1-9]|[12]\\d|3[01])(?:(?:-|\/|\\,)(?:0?[1-9]|[12]\\d|3[01]))?(?:,(?:0?[1-9]|[12]\\d|3[01])" +
"(?:(?:-|\/|\\,)(?:0?[1-9]|[12]\\d|3[01]))?)*)\\s+";
const regMonth = "(\\?|\\*|(?:[1-9]|1[012])(?:(?:-|\/|\\,)(?:[1-9]|1[012]))?(?:L|W)?(?:,(?:[1-9]|1[012])(?:(?:-|\/|\\,)" +
"(?:[1-9]|1[012]))?(?:L|W)?)*|\\?|\\*|(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:(?:-)" +
"(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?(?:,(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)" +
"(?:(?:-)(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)*)\\s+";
const regWeek = "(\\?|\\*|(?:[0-6])(?:(?:-|\/|\\,|#)(?:[0-6]))?(?:L)?(?:,(?:[0-6])(?:(?:-|\/|\\,|#)" +
"(?:[0-6]))?(?:L)?)*|\\?|\\*|(?:MON|TUE|WED|THU|FRI|SAT|SUN)(?:(?:-)(?:MON|TUE|WED|THU|FRI|SAT|SUN))?" +
"(?:,(?:MON|TUE|WED|THU|FRI|SAT|SUN)(?:(?:-)(?:MON|TUE|WED|THU|FRI|SAT|SUN))?)*)(|\\s)+";
const regYear = "(\\?|\\*|(?:|\\d{4})(?:(?:-|\/|\\,)(?:|\\d{4}))?(?:,(?:|\\d{4})(?:(?:-|\/|\\,)(?:|\\d{4}))?)*))$";
const regEx = regSecond + regMinute + regHour + regDay + regMonth + regWeek + regYear;
let reg = new RegExp(regEx, "i");
return reg.test(testValue);
}

View File

@ -1,20 +1,26 @@
<div class="login-wrapper" [ngStyle]="{'background-image': customLoginBgImg? 'url(static/images/' + customLoginBgImg + ')': ''}">
<div class="login-wrapper"
[ngStyle]="{'background-image': customLoginBgImg? 'url(static/images/' + customLoginBgImg + ')': ''}">
<form #signInForm="ngForm" class="login">
<label class="title"> {{customAppTitle? customAppTitle:(appTitle | translate)}}
</label>
<a href="/c/oidc/login" class="login-oidc">
<button type="button" id="log_oidc" class="btn btn-primary btn-block">
<span>{{'BUTTON.LOG_IN_OIDC' | translate }}</span>
</button>
</a>
<div class="login-group">
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left">
<input class="username" type="text" required
[(ngModel)]="signInCredential.principal"
<label for="username" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-md tooltip-top-left">
<input class="username" type="text" required [(ngModel)]="signInCredential.principal"
name="login_username" id="login_username" placeholder='{{"PLACEHOLDER.SIGN_IN_NAME" | translate}}'
#userNameInput='ngModel'>
<span class="tooltip-content">
{{ 'TOOLTIP.SIGN_IN_USERNAME' | translate }}
</span>
</label>
<label for="username" aria-haspopup="true" role="tpopular-repo-wrapperooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left">
<input class="password" type="password" required
[(ngModel)]="signInCredential.password"
<label for="username" aria-haspopup="true" role="tpopular-repo-wrapperooltip"
class="tooltip tooltip-validation tooltip-md tooltip-top-left">
<input class="password" type="password" required [(ngModel)]="signInCredential.password"
name="login_password" id="login_password" placeholder='{{"PLACEHOLDER.SIGN_IN_PWD" | translate}}'
#passwordInput="ngModel">
<span class="tooltip-content">
@ -22,18 +28,23 @@
</span>
</label>
<div class="checkbox">
<input type="checkbox" id="rememberme" #rememberMeBox (click)="clickRememberMe($event)" [checked]="rememberMe">
<input type="checkbox" id="rememberme" #rememberMeBox (click)="clickRememberMe($event)"
[checked]="rememberMe">
<label for="rememberme">{{ 'SIGN_IN.REMEMBER' | translate }}</label>
<a *ngIf="showForgetPwd" href="javascript:void(0)" class="forgot-password-link" (click)="forgotPassword()">{{'SIGN_IN.FORGOT_PWD' | translate}}</a>
<a *ngIf="showForgetPwd" href="javascript:void(0)" class="forgot-password-link"
(click)="forgotPassword()">{{'SIGN_IN.FORGOT_PWD' | translate}}</a>
</div>
<div [class.visibility-hidden]="!isError" class="error active">
{{ 'SIGN_IN.INVALID_MSG' | translate }}
</div>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" (click)="signIn()">{{ 'BUTTON.LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()" *ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary"
(click)="signIn()" id="log_in">{{ 'BUTTON.LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()"
*ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
</div>
<div>
<a href="https://github.com/goharbor/harbor" target="_blank" class="more-info-link">{{ 'BUTTON.MORE_INFO' | translate }}</a>
<a href="https://github.com/goharbor/harbor" target="_blank"
class="more-info-link">{{ 'BUTTON.MORE_INFO' | translate }}</a>
</div>
</form>
<div *ngIf="appConfig.show_popular_repo" id="pop_repo" class="popular-repo-wrapper">

View File

@ -60,3 +60,6 @@
background:transparent;
}
}
.title{
margin-bottom: 50px;
}

View File

@ -32,6 +32,9 @@ import zh from '@angular/common/locales/zh-Hans';
import es from '@angular/common/locales/es';
import localeFr from '@angular/common/locales/fr';
import { DevCenterComponent } from './dev-center/dev-center.component';
import { VulnerabilityPageComponent } from './vulnerability-page/vulnerability-page.component';
import { GcPageComponent } from './gc-page/gc-page.component';
import { OidcOnboardModule } from './oidc-onboard/oidc-onboard.module';
registerLocaleData(zh, 'zh-cn');
registerLocaleData(es, 'es-es');
registerLocaleData(localeFr, 'fr-fr');
@ -51,7 +54,9 @@ export function getCurrentLanguage(translateService: TranslateService) {
@NgModule({
declarations: [
AppComponent,
ProjectConfigComponent
ProjectConfigComponent,
VulnerabilityPageComponent,
GcPageComponent
],
imports: [
BrowserModule,
@ -60,7 +65,8 @@ export function getCurrentLanguage(translateService: TranslateService) {
AccountModule,
HarborRoutingModule,
ConfigurationModule,
DeveloperCenterModule
DeveloperCenterModule,
OidcOnboardModule
],
exports: [
],

View File

@ -46,6 +46,19 @@
</a>
</clr-vertical-nav-group-children>
</clr-vertical-nav-group>
<clr-vertical-nav-group *ngIf="isSystemAdmin" routerLinkActive="active">
<clr-icon shape="event" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.TASKS' | translate}}
<a routerLink="#" hidden aria-hidden="true"></a>
<clr-vertical-nav-group-children *clrIfExpanded="true">
<a clrVerticalNavLink routerLink="/harbor/vulnerability" routerLinkActive="active">
{{'SIDE_NAV.SYSTEM_MGMT.VULNERABILITY' | translate}}
</a>
<a clrVerticalNavLink routerLink="/harbor/gc" routerLinkActive="active">
{{'SIDE_NAV.SYSTEM_MGMT.GARBAGE_COLLECTION' | translate}}
</a>
</clr-vertical-nav-group-children>
</clr-vertical-nav-group>
</div>
<div class="vertical-nav-footer">
<a clrVerticalNavLink target="_blank" routerLink="/devcenter">

View File

@ -9,10 +9,12 @@
<option value="ldap_auth">{{'CONFIG.AUTH_MODE_LDAP' | translate }}</option>
<option value="uaa_auth">{{'CONFIG.AUTH_MODE_UAA' | translate }}</option>
<option *ngIf="showHttpAuth" value="http_auth">{{'CONFIG.AUTH_MODE_HTTP' | translate }}</option>
<option value="oidc_auth">{{'CONFIG.AUTH_MODE_OIDC' | translate }}</option>
</select>
</div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right top-1">
<a href="{{currentConfig?.auth_mode?.value==='oidc_auth'?'https://openid.net/connect/':'javascript:void(0)'}}"
target="{{currentConfig?.auth_mode?.value==='oidc_auth'?'_blank':''}}" role="tooltip"
aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right top-1">
<clr-icon shape="info-circle" size="24" class="info-tips-icon"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.AUTH_MODE' | translate}}</span>
</a>
@ -201,7 +203,8 @@
<div class="form-group">
<label>{{'CONFIG.LDAP.LDAP_GROUP_MEMBERSHIP' | translate}}</label>
<input name="ldapGroupMembership" class="padding-right-28" type="text" #ldapGroupFilterInput="ngModel"
[(ngModel)]="currentConfig.ldap_group_membership_attribute.value" id="ldapGroupMembership" size="40" [disabled]="disabled(currentConfig.ldap_group_membership_attribute)">
[(ngModel)]="currentConfig.ldap_group_membership_attribute.value" id="ldapGroupMembership" size="40"
[disabled]="disabled(currentConfig.ldap_group_membership_attribute)">
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
@ -227,8 +230,8 @@
</clr-tooltip-content>
</clr-tooltip>
</div>
</section>
<section class="form-block">
<div class="form-group" *ngIf="showSelfReg">
<label for="selfReg">{{'CONFIG.SELF_REGISTRATION' | translate}}</label>
@ -238,7 +241,7 @@
[disabled]="disabled(currentConfig.self_registration)" />
<label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-top-right top-7">
class="tooltip tooltip-top-right top-5">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span *ngIf="checkable; else elseBlock" class="tooltip-content">{{'CONFIG.TOOLTIP.SELF_REGISTRATION_ENABLE'
| translate}}</span>
@ -260,7 +263,7 @@
(ngModelChange)="setVerifyCertValue($event)" />
<label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-top-right top-7">
class="tooltip tooltip-top-right top-5">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_CERT' | translate}}</span>
</a>
@ -271,10 +274,13 @@
<section class="form-block" *ngIf="showHttpAuth">
<div class="form-group">
<label for="http_authproxy_endpoint" class="required">{{'CONFIG.HTTP_AUTH.ENDPOINT' | translate}}</label>
<label for="http_authproxy_endpoint" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right"
<label for="http_authproxy_endpoint" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-md tooltip-top-right"
[class.invalid]="httpAuthproxyEndpointInput.invalid && (httpAuthproxyEndpointInput.dirty || httpAuthproxyEndpointInput.touched)">
<input type="text" pattern="^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(.*?)*$" #httpAuthproxyEndpointInput="ngModel" required id="http_authproxy_endpoint" name="http_authproxy_endpoint" size="35"
[(ngModel)]="currentConfig.http_authproxy_endpoint.value" [disabled]="!currentConfig.http_authproxy_endpoint.editable" >
<input type="text" pattern="^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(.*?)*$"
#httpAuthproxyEndpointInput="ngModel" required id="http_authproxy_endpoint"
name="http_authproxy_endpoint" size="35" [(ngModel)]="currentConfig.http_authproxy_endpoint.value"
[disabled]="!currentConfig.http_authproxy_endpoint.editable">
<span class="tooltip-content">
{{'TOOLTIP.ENDPOINT_FORMAT' | translate}}
</span>
@ -284,8 +290,10 @@
<label for="http_authproxy_skip_cert_verify"
class="required">{{'CONFIG.HTTP_AUTH.VERIFY_CERT' | translate}}</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox name="http_authproxy_skip_cert_verify" id="http_authproxy_skip_cert_verify"
[(ngModel)]="currentConfig.http_authproxy_skip_cert_verify.value" [disabled]="!currentConfig.http_authproxy_skip_cert_verify.editable" />
<input type="checkbox" clrCheckbox name="http_authproxy_skip_cert_verify"
id="http_authproxy_skip_cert_verify"
[(ngModel)]="currentConfig.http_authproxy_skip_cert_verify.value"
[disabled]="!currentConfig.http_authproxy_skip_cert_verify.editable" />
</clr-checkbox-wrapper>
</div>
<div class="form-group">
@ -293,11 +301,108 @@
class="required">{{'CONFIG.HTTP_AUTH.ALWAYS_ONBOARD' | translate}}</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox name="http_authproxy_always_onboard"
id="http_authproxy_always_onboard" [disabled]="!currentConfig.http_authproxy_always_onboard.editable"
id="http_authproxy_always_onboard"
[disabled]="!currentConfig.http_authproxy_always_onboard.editable"
[(ngModel)]="currentConfig.http_authproxy_always_onboard.value" />
</clr-checkbox-wrapper>
</div>
</section>
<section class="form-block" *ngIf="showOIDC">
<div class="form-group">
<label for="oidcName" class="required">{{'CONFIG.OIDC.OIDC_PROVIDER' | translate}}</label>
<label for="oidcName" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-lg tooltip-top-right"
[class.invalid]="oidcNameInput.invalid && (oidcNameInput.dirty || oidcNameInput.touched)">
<input name="oidcName" required type="text" #oidcNameInput="ngModel"
[(ngModel)]="currentConfig.oidc_name.value" required id="oidcName" size="40"
[disabled]="disabled(currentConfig.oidc_name)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right top-1">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'TOOLTIP.OIDC_NAME' | translate}}</span>
</a>
</div>
<div class="form-group">
<label for="oidcEndpoint" class="required">{{'CONFIG.OIDC.ENDPOINT' | translate}}</label>
<label for="oidcEndpoint" aria-haspopup="true" role="tooltip"
[class.invalid]="oidcEndpointInput.invalid && (oidcEndpointInput.dirty || oidcEndpointInput.touched)"
class="tooltip tooltip-validation tooltip-lg tooltip-top-right">
<input name="oidcEndpoint" type="text" #oidcEndpointInput="ngModel" required
[(ngModel)]="currentConfig.oidc_endpoint.value" id="oidcEndpoint" size="40"
[disabled]="disabled(currentConfig.oidc_endpoint)" pattern="^([hH][tT]{2}[pP][sS])(.*?)*$">
<span class="tooltip-content">
{{'TOOLTIP.OIDC_ENDPOIT_FORMAT' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right top-1">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'TOOLTIP.OIDC_ENDPOINT' | translate}}</span>
</a>
</div>
<div class="form-group">
<label for="oidcClientId" class="required">{{'CONFIG.OIDC.CLIENT_ID' | translate}}</label>
<label for="oidcClientId" aria-haspopup="true" role="tooltip"
[class.invalid]="oidcClientIdInput.invalid && (oidcClientIdInput.dirty || oidcClientIdInput.touched)"
class="tooltip tooltip-validation tooltip-lg tooltip-top-right">
<input name="oidcClientId" type="text" #oidcClientIdInput="ngModel" required
[(ngModel)]="currentConfig.oidc_client_id.value" id="oidcClientId" size="40"
[disabled]="disabled(currentConfig.oidc_client_id)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="oidcClientSecret" class="required">{{'CONFIG.OIDC.CLIENTSECRET' | translate}}</label>
<label for="oidcClientSecret" aria-haspopup="true" role="tooltip"
[class.invalid]="oidcClientSecretInput.invalid && (oidcClientSecretInput.dirty || oidcClientSecretInput.touched)"
class="tooltip tooltip-validation tooltip-lg tooltip-top-right">
<input name="oidcClientSecret" type="password" #oidcClientSecretInput="ngModel" required
[(ngModel)]="currentConfig.oidc_client_secret.value" id="oidcClientSecret" size="40"
[disabled]="disabled(currentConfig.oidc_client_secret)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="oidcScope" class="required">{{'CONFIG.OIDC.SCOPE' | translate}}</label>
<label for="oidcScope" aria-haspopup="true" role="tooltip"
[class.invalid]="oidcScopeInput.invalid && (oidcScopeInput.dirty || oidcScopeInput.touched)"
class="tooltip tooltip-validation tooltip-lg tooltip-top-right">
<input name="oidcScope" type="text" #oidcScopeInput="ngModel"
[(ngModel)]="currentConfig.oidc_scope.value" id="oidcScope" size="40" required
[disabled]="disabled(currentConfig.oidc_scope)" pattern="^(\w+,){0,}openid(,\w+){0,}$">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right top-1">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'TOOLTIP.OIDC_SCOPE' | translate}}</span>
</a>
</div>
<div class="form-group">
<label for="oidc_skip_cert_verify">{{'CONFIG.OIDC.OIDCSKIPCERTVERIFY' | translate}}</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox name="oidc_skip_cert_verify" id="oidc_skip_cert_verify"
[disabled]="disabled(currentConfig.oidc_skip_cert_verify)"
[(ngModel)]="currentConfig.oidc_skip_cert_verify.value" />
</clr-checkbox-wrapper>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right top-1px">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'TOOLTIP.OIDC_SKIPCERTVERIFY' | translate}}</span>
</a>
</div>
</section>
</form>
<div>
<button type="button" id="config_auth_save" class="btn btn-primary" (click)="save()"

View File

@ -9,7 +9,9 @@ clr-tooltip {
.top-1 {
top: -1px;
}
.top-7 {
top: -7px;
.top-1px {
top: 1px;
}
.top-5 {
top: -5px;
}

View File

@ -15,7 +15,7 @@ import { Component, Input, ViewChild, SimpleChanges, OnChanges} from '@angular/c
import { NgForm } from '@angular/forms';
import { Subscription } from "rxjs";
import { Configuration, clone, isEmpty, getChanges, StringValueItem} from '@harbor/ui';
import { Configuration, clone, isEmpty, getChanges, StringValueItem, BoolValueItem } from '@harbor/ui';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { ConfirmMessageHandler } from '../config.msg.utils';
import { AppConfigService } from '../../app-config.service';
@ -53,7 +53,9 @@ export class ConfigurationAuthComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes["currentConfig"]) {
this.originalConfig = clone(this.currentConfig);
}
}
@ -66,6 +68,9 @@ export class ConfigurationAuthComponent implements OnChanges {
public get showUAA(): boolean {
return this.currentConfig && this.currentConfig.auth_mode && this.currentConfig.auth_mode.value === 'uaa_auth';
}
public get showOIDC(): boolean {
return this.currentConfig && this.currentConfig.auth_mode && this.currentConfig.auth_mode.value === 'oidc_auth';
}
public get showHttpAuth(): boolean {
return this.currentConfig && this.currentConfig.auth_mode && this.currentConfig.auth_mode.value === 'http_auth';
}
@ -74,7 +79,7 @@ export class ConfigurationAuthComponent implements OnChanges {
return true;
} else {
return this.currentConfig.auth_mode.value !== 'ldap_auth' && this.currentConfig.auth_mode.value !== 'uaa_auth'
&& this.currentConfig.auth_mode.value !== 'http_auth' ;
&& this.currentConfig.auth_mode.value !== 'http_auth' && this.currentConfig.auth_mode.value !== 'oidc_auth';
}
}
@ -168,7 +173,7 @@ export class ConfigurationAuthComponent implements OnChanges {
handleOnChange($event: any): void {
if ($event && $event.target && $event.target["value"]) {
let authMode = $event.target["value"];
if (authMode === 'ldap_auth' || authMode === 'uaa_auth' || authMode === 'http_auth') {
if (authMode === 'ldap_auth' || authMode === 'uaa_auth' || authMode === 'http_auth' || authMode === 'oidc_auth') {
if (this.currentConfig.self_registration.value) {
this.currentConfig.self_registration.value = false; // unselect
}
@ -213,6 +218,7 @@ export class ConfigurationAuthComponent implements OnChanges {
// Add two password fields
configurations.ldap_search_password = new StringValueItem(fakePass, true);
configurations.uaa_client_secret = new StringValueItem(fakePass, true);
configurations.oidc_client_secret = new StringValueItem(fakePass, true);
this.currentConfig = configurations;
// Keep the original copy of the data
this.originalConfig = clone(configurations);

View File

@ -32,22 +32,6 @@
</clr-tab-content>
</ng-template>
</clr-tab>
<clr-tab *ngIf="withClair">
<button id="config-vulnerability" clrTabLink>{{'CONFIG.VULNERABILITY' | translate }}</button>
<ng-template [(clrIfActive)]="vulnerabilityActive">
<clr-tab-content id="vulnerability" *ngIf="withClair">
<vulnerability-config></vulnerability-config>
</clr-tab-content>
</ng-template>
</clr-tab>
<clr-tab *ngIf="hasAdminRole">
<button id="config-gc" clrTabLink>{{'CONFIG.GC' | translate }}</button>
<ng-template [(clrIfActive)]="gcActive">
<clr-tab-content id="gc" *ngIf="hasAdminRole">
<gc-config></gc-config>
</clr-tab-content>
</ng-template>
</clr-tab>
</clr-tabs>
</div>
</div>

View File

@ -14,8 +14,8 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { Subscription } from "rxjs";
import {
Configuration, StringValueItem, SystemSettingsComponent, VulnerabilityConfigComponent,
isEmpty, clone, getChanges, GcComponent, GcRepoService } from '@harbor/ui';
Configuration, StringValueItem, SystemSettingsComponent,
isEmpty, clone, getChanges, GcRepoService } from '@harbor/ui';
import { ConfirmationTargets, ConfirmationState } from '../shared/shared.const';
import { SessionService } from '../shared/session.service';
@ -34,8 +34,6 @@ const TabLinkContentMap = {
'config-replication': 'replication',
'config-email': 'email',
'config-system': 'system_settings',
'config-vulnerability': 'vulnerability',
'config-gc': 'gc',
'config-label': 'system_label',
};
@ -52,8 +50,6 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
confirmSub: Subscription;
@ViewChild(SystemSettingsComponent) systemSettingsConfig: SystemSettingsComponent;
@ViewChild(VulnerabilityConfigComponent) vulnerabilityConfig: VulnerabilityConfigComponent;
@ViewChild(GcComponent) gcConfig: GcComponent;
@ViewChild(ConfigurationEmailComponent) mailConfig: ConfigurationEmailComponent;
@ViewChild(ConfigurationAuthComponent) authConfig: ConfigurationAuthComponent;
@ -73,10 +69,6 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
return this.appConfigService.getConfig().has_ca_root;
}
public get withClair(): boolean {
return this.appConfigService.getConfig().with_clair;
}
public get withAdmiral(): boolean {
return this.appConfigService.getConfig().with_admiral;
}
@ -147,6 +139,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
configurations.email_password = new StringValueItem(fakePass, true);
configurations.ldap_search_password = new StringValueItem(fakePass, true);
configurations.uaa_client_secret = new StringValueItem(fakePass, true);
configurations.oidc_client_secret = new StringValueItem(fakePass, true);
this.allConfig = configurations;
// Keep the original copy of the data
this.originalCopy = clone(configurations);

View File

@ -0,0 +1,23 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h2 class="custom-h2 gc-title">{{'CONFIG.GC' | translate }}</h2>
<clr-tabs>
<clr-tab *ngIf="hasAdminRole">
<button id="config-gc" clrTabLink>{{'CONFIG.GC' | translate }}</button>
<ng-template [(clrIfActive)]="gcActive">
<clr-tab-content id="gc" *ngIf="hasAdminRole">
<gc-config></gc-config>
</clr-tab-content>
</ng-template>
</clr-tab>
<clr-tab *ngIf="hasAdminRole">
<button id="gc-log" clrTabLink>{{'CONFIG.HISTORY' | translate }}</button>
<ng-template [(clrIfActive)]="historyActive">
<clr-tab-content id="history" *ngIf="hasAdminRole">
<gc-history></gc-history>
</clr-tab-content>
</ng-template>
</clr-tab>
</clr-tabs>
</div>
</div>

View File

@ -0,0 +1,3 @@
.gc-title {
display: inline-block;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GcPageComponent } from './gc-page.component';
describe('GcPageComponent', () => {
let component: GcPageComponent;
let fixture: ComponentFixture<GcPageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ GcPageComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GcPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,19 @@
import { Component, OnInit } from "@angular/core";
import { SessionService } from "../shared/session.service";
@Component({
selector: "app-gc-page",
templateUrl: "./gc-page.component.html",
styleUrls: ["./gc-page.component.scss"]
})
export class GcPageComponent implements OnInit {
constructor(private session: SessionService) {}
ngOnInit() {}
public get hasAdminRole(): boolean {
return (
this.session.getCurrentUser() &&
this.session.getCurrentUser().has_admin_role
);
}
}

View File

@ -18,11 +18,14 @@ import { SystemAdminGuard } from './shared/route/system-admin-activate.service';
import { AuthCheckGuard } from './shared/route/auth-user-activate.service';
import { SignInGuard } from './shared/route/sign-in-guard-activate.service';
import { MemberGuard } from './shared/route/member-guard-activate.service';
import { OidcGuard } from './shared/route/oidc-guard-active.service';
import { PageNotFoundComponent } from './shared/not-found/not-found.component';
import { HarborShellComponent } from './base/harbor-shell/harbor-shell.component';
import { ConfigurationComponent } from './config/config.component';
import { DevCenterComponent } from './dev-center/dev-center.component';
import { GcPageComponent } from './gc-page/gc-page.component';
import { VulnerabilityPageComponent } from './vulnerability-page/vulnerability-page.component';
import { UserComponent } from './user/user.component';
import { SignInComponent } from './account/sign-in/sign-in.component';
@ -53,6 +56,7 @@ import { ProjectRoutingResolver } from './project/project-routing-resolver.servi
import { ListChartsComponent } from './project/helm-chart/list-charts.component';
import { ListChartVersionsComponent } from './project/helm-chart/list-chart-versions/list-chart-versions.component';
import { HelmChartDetailComponent } from './project/helm-chart/helm-chart-detail/chart-detail.component';
import { OidcOnboardComponent } from './oidc-onboard/oidc-onboard.component';
const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
@ -61,6 +65,11 @@ const harborRoutes: Routes = [
path: 'devcenter',
component: DevCenterComponent
},
{
path: 'oidc-onboard',
component: OidcOnboardComponent,
canActivate: [OidcGuard, SignInGuard]
},
{
path: 'harbor',
component: HarborShellComponent,
@ -199,6 +208,16 @@ const harborRoutes: Routes = [
component: ConfigurationComponent,
canActivate: [SystemAdminGuard]
},
{
path: 'vulnerability',
component: VulnerabilityPageComponent,
canActivate: [SystemAdminGuard]
},
{
path: 'gc',
component: GcPageComponent,
canActivate: [SystemAdminGuard]
},
{
path: 'registry',
component: DestinationPageComponent,

View File

@ -0,0 +1,43 @@
<div class="modal">
<div class="modal-dialog" role="dialog">
<div class="modal-content">
<div class="modal-header">
<button (click)="backHarborPage()" class="close">
<clr-icon shape="close"></clr-icon>
</button>
<h3 class="modal-title oidc-header-text"><span>{{'CONFIG.OIDC.OIDC_SETNAME' | translate}}</span>&nbsp;
</h3>
</div>
<div id="error-message">
<div class="alert alert-danger" role="alert" *ngIf="errorOpen">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<clr-icon class="alert-icon" size="24" shape="exclamation-circle"></clr-icon>
</div>
<span class="alert-text">{{errorMessage}}</span>
</div>
</div>
<button type="button" class="close" aria-label="Close" (click)="emptyErrorMessage()">
<clr-icon aria-hidden="true" size="16" shape="close"></clr-icon>
</button>
</div>
</div>
<div class="modal-body">
<p class="body-message">{{'CONFIG.OIDC.OIDC_SETNAMECONTENT' | translate}}</p>
</div>
<br />
<div class="username-div">
<label for="oidcUsername" class="required">{{'CONFIG.OIDC.OIDC_USERNAME' | translate}}</label>
<label for="oidcUsername" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right">
<input name="oidcUsername" type="text" [formControl]="oidcUsername" required id="oidcUsername" size="40">
</label>
</div>
<div class="modal-footer">
<button class="btn btn-outline" type="button" (click)="backHarborPage()" id="cancelButton">{{'BUTTON.CANCEL' | translate }}</button>
<button class="btn btn-primary" id="saveButton" (click)="clickSaveBtn()" [disabled]="oidcUsername.invalid"
type="button">{{'BUTTON.SAVE' | translate }}</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,25 @@
.modal {
background-color: rgb(80, 80, 80);
.body-message {
margin-top: 10px;
}
.modal-header {
.close {
margin-right: 0.2rem
}
}
.username-div {
display: flex;
justify-content: space-between;
width: 80%;
}
input {
width: 300px;
}
}
.oidc-header-text{
color:rgb(94, 94, 94);
}
.close-error {
padding-right:0;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { OidcOnboardComponent } from './oidc-onboard.component';
describe('OidcOnboardComponent', () => {
let component: OidcOnboardComponent;
let fixture: ComponentFixture<OidcOnboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ OidcOnboardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OidcOnboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,43 @@
import { Router, ActivatedRoute } from '@angular/router';
import { Component, OnInit } from '@angular/core';
import { OidcOnboardService } from './oidc-onboard.service';
import { FormControl } from '@angular/forms';
import { errorHandler } from "../shared/shared.utils";
import { CommonRoutes } from '../shared/shared.const';
@Component({
selector: 'app-oidc-onboard',
templateUrl: './oidc-onboard.component.html',
styleUrls: ['./oidc-onboard.component.scss']
})
export class OidcOnboardComponent implements OnInit {
url: string;
errorMessage: string = '';
oidcUsername = new FormControl('');
errorOpen: boolean = false;
constructor(
private oidcOnboardService: OidcOnboardService,
private router: Router,
private route: ActivatedRoute,
) { }
ngOnInit() {
this.route.queryParams
.subscribe(params => {
this.oidcUsername.setValue(params["username"] || "");
});
}
clickSaveBtn(): void {
this.oidcOnboardService.oidcSave({ username: this.oidcUsername.value }).subscribe(res => { }
, error => {
this.errorMessage = errorHandler(error);
this.errorOpen = true;
});
}
emptyErrorMessage() {
this.errorOpen = false;
}
backHarborPage() {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
}
}

View File

@ -0,0 +1,29 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { OidcOnboardComponent } from './oidc-onboard.component';
import { SharedModule } from '../shared/shared.module';
import { OidcOnboardService } from './oidc-onboard.service';
@NgModule({
imports: [SharedModule],
declarations: [
OidcOnboardComponent,
],
providers: [OidcOnboardService],
exports: [
OidcOnboardComponent
]
})
export class OidcOnboardModule { }

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { OidcOnboardService } from './oidc-onboard.service';
describe('OidcOnboardService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: OidcOnboardService = TestBed.get(OidcOnboardService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
import { Http, URLSearchParams } from '@angular/http';
import { catchError } from 'rxjs/operators';
import { throwError as observableThrowError, Observable } from 'rxjs';
export const logEndpoint = "/c/oidc/onboard";
@Injectable()
export class OidcOnboardService {
constructor(private http: Http) { }
oidcSave(param): Observable<any> {
return this.http.post(logEndpoint, param).pipe(catchError(error => observableThrowError(error)));
}
}

View File

@ -85,15 +85,9 @@ export class ProjectDetailComponent implements OnInit {
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.LABEL.KEY, USERSTATICPERMISSION.LABEL.VALUE.CREATE));
forkJoin(...permissionsList).subscribe(Rules => {
this.hasLogListPermission = Rules[0] as boolean;
this.hasConfigurationListPermission = Rules[1] as boolean;
this.hasMemberListPermission = Rules[2] as boolean;
this.hasReplicationListPermission = Rules[3] as boolean;
this.hasLabelListPermission = Rules[4] as boolean;
this.hasRepositoryListPermission = Rules[5] as boolean;
this.hasHelmChartsListPermission = Rules[6] as boolean;
this.hasRobotListPermission = Rules[7] as boolean;
this.hasLabelCreatePermission = Rules[8] as boolean;
[this.hasLogListPermission, this.hasConfigurationListPermission, this.hasMemberListPermission, this.hasReplicationListPermission
, this.hasLabelListPermission, this.hasRepositoryListPermission, this.hasHelmChartsListPermission, this.hasRobotListPermission
, this.hasLabelCreatePermission] = Rules;
}, error => this.errorHandler.error(error));
}

View File

@ -0,0 +1,58 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import {
CanActivate, Router,
ActivatedRouteSnapshot,
RouterStateSnapshot,
CanActivateChild
} from '@angular/router';
import { AppConfigService } from '../../app-config.service';
import { UserPermissionService } from "@harbor/ui";
import { Observable, of } from 'rxjs';
import { CommonRoutes } from '../../shared/shared.const';
@Injectable()
export class OidcGuard implements CanActivate, CanActivateChild {
constructor(private appConfigService: AppConfigService, private router: Router, private userPermission: UserPermissionService) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
// If user has logged in, should not login again
return new Observable((observer) => {
// If signout appended
let queryParams = route.queryParams;
this.appConfigService.load()
.subscribe(updatedConfig => {
if (updatedConfig.auth_mode === 'oidc_auth') {
return observer.next(true);
} else {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
return observer.next(false);
}
}
, error => {
// Catch the error
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
console.error("Failed to load bootstrap options with error: ", error);
return observer.next(false);
});
});
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
return this.canActivate(route, state);
}
}

View File

@ -33,6 +33,7 @@ import { AuthCheckGuard } from "./route/auth-user-activate.service";
import { SignInGuard } from "./route/sign-in-guard-activate.service";
import { SystemAdminGuard } from "./route/system-admin-activate.service";
import { MemberGuard } from "./route/member-guard-activate.service";
import { OidcGuard } from "./route/oidc-guard-active.service";
import { LeavingRepositoryRouteDeactivate } from "./route/leaving-repository-deactivate.service";
import { PortValidatorDirective } from "./port.directive";
@ -139,6 +140,7 @@ const uiLibConfig: IServiceConfig = {
SignInGuard,
LeavingRepositoryRouteDeactivate,
MemberGuard,
OidcGuard,
MessageHandlerService,
StatisticHandler
]

View File

@ -0,0 +1,15 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h2 class="custom-h2 vul-title">{{'VULNERABILITY.SINGULAR' | translate }}</h2>
<clr-tabs>
<clr-tab *ngIf="withClair">
<button id="config-vulnerability" clrTabLink>{{'CONFIG.VULNERABILITY' | translate }}</button>
<ng-template [(clrIfActive)]="vulnerabilityActive">
<clr-tab-content id="vulnerability" *ngIf="withClair">
<vulnerability-config></vulnerability-config>
</clr-tab-content>
</ng-template>
</clr-tab>
</clr-tabs>
</div>
</div>

View File

@ -0,0 +1,3 @@
.vul-title {
display: inline-block;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { VulnerabilityPageComponent } from './vulnerability-page.component';
describe('VulnerabilityPageComponent', () => {
let component: VulnerabilityPageComponent;
let fixture: ComponentFixture<VulnerabilityPageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ VulnerabilityPageComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(VulnerabilityPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,17 @@
import { Component, OnInit } from "@angular/core";
import { AppConfigService } from "../app-config.service";
@Component({
selector: "vulnerability-page",
templateUrl: "./vulnerability-page.component.html",
styleUrls: ["./vulnerability-page.component.scss"]
})
export class VulnerabilityPageComponent implements OnInit {
constructor(private appConfigService: AppConfigService) {}
ngOnInit() {}
public get withClair(): boolean {
return this.appConfigService.getConfig().with_clair;
}
}

View File

@ -22,6 +22,7 @@
"OK": "OK",
"DELETE": "DELETE",
"LOG_IN": "LOG IN",
"LOG_IN_OIDC": "Login via OIDC provider",
"SIGN_UP_LINK": "Sign up for an account",
"SIGN_UP": "SIGN UP",
"CONFIRM": "CONFIRM",
@ -73,7 +74,12 @@
"EMPTY": "Name is required",
"NONEMPTY": "Can't be empty",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode.",
"ENDPOINT_FORMAT": "Endpoint must start with HTTP or HTTPS."
"ENDPOINT_FORMAT": "Endpoint must start with HTTP or HTTPS.",
"OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS.",
"OIDC_NAME": "The name of the OIDC provider.",
"OIDC_ENDPOINT": "The URL of an OIDC-complaint server.",
"OIDC_SCOPE": "The scope sent to OIDC server during authentication. It has to contain “openid”, and “offline_access”. If you are using google, please remove “offline_access” from this field.",
"OIDC_SKIPCERTVERIFY": "Check this box if your OIDC server is hosted via self-signed certificate."
},
"PLACEHOLDER": {
"CURRENT_PWD": "Enter current password",
@ -125,9 +131,12 @@
"GROUP": "Groups",
"REGISTRY": "Registries",
"REPLICATION": "Replications",
"CONFIG": "Configuration"
"CONFIG": "Configuration",
"VULNERABILITY": "Vulnerability",
"GARBAGE_COLLECTION": "Garbage Collection"
},
"LOGS": "Logs",
"TASKS": "Tasks",
"API_EXPLORER": "API EXPLORER"
},
"USER": {
@ -622,6 +631,7 @@
"SUB_TITLE_SUFIX": "logs"
},
"CONFIG": {
"HISTORY": "History",
"TITLE": "Configuration",
"AUTH": "Authentication",
"REPLICATION": "Replication",
@ -653,6 +663,7 @@
"AUTH_MODE_LDAP": "LDAP",
"AUTH_MODE_UAA": "UAA",
"AUTH_MODE_HTTP": "http_auth",
"AUTH_MODE_OIDC": "OIDC",
"SCOPE_BASE": "Base",
"SCOPE_ONE_LEVEL": "OneLevel",
"SCOPE_SUBTREE": "Subtree",
@ -716,6 +727,17 @@
"ALWAYS_ONBOARD": "Always Onboard",
"VERIFY_CERT": "Authentication Verify Cert"
},
"OIDC": {
"OIDC_PROVIDER": "OIDC provider",
"ENDPOINT": "OIDC Endpoint",
"CLIENT_ID": "OIDC Client ID",
"CLIENTSECRET": "OIDC Client Secret",
"SCOPE": "OIDC Scope",
"OIDCSKIPCERTVERIFY": "OIDC Verify Cert",
"OIDC_SETNAME": "Set OIDC Username",
"OIDC_SETNAMECONTENT": "You must create a Harbor username the first time when authenticating via a third party(OIDC).This will be used within Harbor to be associated with projects, roles, etc.",
"OIDC_USERNAME": "Username"
},
"SCANNING": {
"TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!",
"TRIGGER_SCAN_ALL_FAIL": "Failed to trigger scan all with error: {{error}",
@ -932,7 +954,7 @@
"GC": {
"CURRENT_SCHEDULE": "Current Schedule",
"GC_NOW": "GC NOW",
"JOB_HISTORY": "GC Jobs History",
"JOB_HISTORY": "GC History",
"JOB_ID": "Job ID",
"TRIGGER_TYPE": "Trigger Type",
"LATEST_JOBS": "Latest {{param}} Jobs",

View File

@ -22,6 +22,7 @@
"OK": "OK",
"DELETE": "ELIMINAR",
"LOG_IN": "IDENTIFICARSE",
"LOG_IN_OIDC": "Login Via OIDC proveedor",
"SIGN_UP_LINK": "Registrar una cuenta",
"SIGN_UP": "REGISTRARSE",
"CONFIRM": "CONFIRMAR",
@ -73,7 +74,12 @@
"EMPTY": "Name is required",
"NONEMPTY": "Can't be empty",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode.",
"ENDPOINT_FORMAT": "Endpoint must start with HTTP or HTTPS."
"ENDPOINT_FORMAT": "Endpoint must start with HTTP or HTTPS.",
"OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS.",
"OIDC_NAME": "El nombre de la OIDC proveedor.",
"OIDC_ENDPOINT": "La dirección URL de un servidor OIDC denuncia.",
"OIDC_SCOPE": "El ámbito de aplicación enviada a OIDC Server durante la autenticación.Tiene que contener 'Openid', y 'offline_access'.Si usted esta usando Google, por favor quitar 'offline_access' de este campo",
"OIDC_SKIPCERTVERIFY": "Marque esta casilla si tu OIDC servidor está alojado a través de certificado autofirmado."
},
"PLACEHOLDER": {
"CURRENT_PWD": "Introduzca la contraseña actual",
@ -125,9 +131,12 @@
"REGISTRY": "Registries",
"GROUP": "Groups",
"REPLICATION": "Replicacións",
"CONFIG": "Configuración"
"CONFIG": "Configuración",
"VULNERABILITY": "Vulnerability",
"GARBAGE_COLLECTION": "Garbage Collection"
},
"LOGS": "Logs",
"TASKS": "Tasks",
"API_EXPLORER": "API EXPLORER"
},
"USER": {
@ -654,6 +663,7 @@
"AUTH_MODE_LDAP": "LDAP",
"AUTH_MODE_UAA": "UAA",
"AUTH_MODE_HTTP": "http_auth",
"AUTH_MODE_OIDC": "OIDC",
"SCOPE_BASE": "Base",
"SCOPE_ONE_LEVEL": "UnNivel",
"SCOPE_SUBTREE": "Subárbol",
@ -717,6 +727,17 @@
"ALWAYS_ONBOARD": "Always Onboard",
"VERIFY_CERT": "Authentication Verify Cert"
},
"OIDC": {
"OIDC_PROVIDER": "OIDC Proveedor",
"ENDPOINT": "OIDC Endpoint",
"CLIENT_ID": "ID de cliente OIDC",
"CLIENTSECRET": "OIDC Client Secret",
"SCOPE": "OIDC Ámbito",
"OIDCSKIPCERTVERIFY": "OIDC Verify Cert",
"OIDC_SETNAME": "Set OIDC nombre de usuario",
"OIDC_SETNAMECONTENT": "Usted debe crear un Harbor nombre de usuario la primera vez cuando la autenticación a través de un tercero (OIDC). Esta será usada en Harbor para ser asociados con proyectos, funciones, etc.",
"OIDC_USERNAME": "Usuario"
},
"SCANNING": {
"TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!",
"TRIGGER_SCAN_ALL_FAIL": "Failed to trigger scan all with error: {{error}",
@ -931,7 +952,7 @@
"GC": {
"CURRENT_SCHEDULE": "Current Schedule",
"GC_NOW": "GC NOW",
"JOB_HISTORY": "GC Jobs History",
"JOB_HISTORY": "GC History",
"JOB_ID": "Job ID",
"TRIGGER_TYPE": "Trigger Type",
"LATEST_JOBS": "Latest {{param}} Jobs",

View File

@ -22,6 +22,7 @@
"OK": "OK",
"DELETE": "SUPPRIMER",
"LOG_IN": "S'IDENTIFIER",
"LOG_IN_OIDC": "Connexion via OIDC fournisseur",
"SIGN_UP_LINK": "Ouvrir un compte",
"SIGN_UP": "S'INSCRIRE",
"CONFIRM": "CONFIRMER",
@ -60,7 +61,12 @@
"USER_EXISTING": "Le nom d'utilisateur est déjà utilisé.",
"NONEMPTY": "Can't be empty",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode.",
"ENDPOINT_FORMAT": "Endpoint must start with HTTP or HTTPS."
"ENDPOINT_FORMAT": "Endpoint must start with HTTP or HTTPS.",
"OIDC_ENDPOIT_FORMAT": "Endpoint must start with HTTPS.",
"OIDC_NAME": "le nom du fournisseur de oidc.",
"OIDC_ENDPOINT": "l'url d'un serveur oidc plainte.",
"OIDC_SCOPE": "le champ envoyés au serveur au cours oidc l'authentification.il doit contenir 'openid', et 'offline_access'.si vous utilisez google, veuillez supprimer 'offline_access' dans ce domaine",
"OIDC_SKIPCERTVERIFY": "cocher cette case si votre oidc serveur est accueilli par auto - certificat signé."
},
"PLACEHOLDER": {
"CURRENT_PWD": "Entrez le mot de passe actuel",
@ -111,9 +117,12 @@
"USER": "Utilisateurs",
"GROUP": "Groups",
"REPLICATION": "Réplication",
"CONFIG": "Configuration"
"CONFIG": "Configuration",
"VULNERABILITY": "Vulnerability",
"GARBAGE_COLLECTION": "Garbage Collection"
},
"LOGS": "Logs",
"TASKS": "Tasks",
"API_EXPLORER": "API EXPLORER"
},
"USER": {
@ -626,6 +635,7 @@
"AUTH_MODE_DB": "Base de données",
"AUTH_MODE_LDAP": "LDAP",
"AUTH_MODE_HTTP": "http_auth",
"AUTH_MODE_OIDC": "OIDC",
"SCOPE_BASE": "Base",
"SCOPE_ONE_LEVEL": "Premier Niveau",
"SCOPE_SUBTREE": "Sous-arbre",
@ -682,6 +692,17 @@
"ALWAYS_ONBOARD": "Always Onboard",
"VERIFY_CERT": "authentification vérifier cert"
},
"OIDC": {
"OIDC_PROVIDER": "OIDC fournisseur",
"ENDPOINT": "OIDC paramètre",
"CLIENT_ID": "no d'identification du client OIDC",
"CLIENTSECRET": "OIDC Client Secret",
"SCOPE": "OIDC Scope",
"OIDCSKIPCERTVERIFY": "OIDC vérifier cert",
"OIDC_SETNAME": "Ensemble OIDC nom d'utilisateur",
"OIDC_SETNAMECONTENT": "vous devez créer un Harbor identifiant la première fois lors de la vérification par une tierce partie (oidc). il sera utilisé au sein de port à être associés aux projets, des rôles, etc.",
"OIDC_USERNAME": "d'utilisateur"
},
"SCANNING": {
"TRIGGER_SCAN_ALL_SUCCESS": "Déclenchement d'analyse globale avec succès !",
"TRIGGER_SCAN_ALL_FAIL": "Echec du déclenchement d'analyse globale avec des erreurs : {{error}",
@ -894,7 +915,7 @@
"GC": {
"CURRENT_SCHEDULE": "Current Schedule",
"GC_NOW": "GC NOW",
"JOB_HISTORY": "GC Jobs History",
"JOB_HISTORY": "GC History",
"JOB_ID": "Job ID",
"TRIGGER_TYPE": "Trigger Type",
"LATEST_JOBS": "Latest {{param}} Jobs",

View File

@ -22,6 +22,7 @@
"OK": "OK",
"DELETE": "DELETAR",
"LOG_IN": "LOG IN",
"LOG_IN_OIDC": "Login via OIDC provedor",
"SIGN_UP_LINK": "Registre-se para uma conta",
"SIGN_UP": "REGISTRE-Se",
"CONFIRM": "CONFIRMAR",
@ -71,7 +72,12 @@
"USER_EXISTING": "Nome de usuário já está em uso.",
"RULE_USER_EXISTING": "Nome já em uso.",
"EMPTY": "Nome é obrigatório",
"ENDPOINT_FORMAT": "Avaliação deve começar por HTTP Ou HTTPS."
"ENDPOINT_FORMAT": "Avaliação deve começar por HTTP Ou HTTPS.",
"OIDC_ENDPOIT_FORMAT": "Avaliação deve começar por HTTPS.",
"OIDC_NAME": "O Nome do prestador de oidc.",
"OIDC_ENDPOINT": "A URL de um servidor oidc denúncia.",
"OIDC_SCOPE": "O âmbito de aplicação enviada Ao servidor oidc Durante a autenticação.TEM que conter 'openid' e 'offline_access'.Se você está usando o Google, por favor remova 'offline_access' desse Campo.",
"OIDC_SKIPCERTVERIFY": "Assinale esta opção se o SEU servidor está hospedado oidc via self - signed certificate."
},
"PLACEHOLDER": {
"CURRENT_PWD": "Insira a senha atual",
@ -123,9 +129,12 @@
"GROUP": "Grupos",
"REGISTRY": "Registros",
"REPLICATION": "Replicações",
"CONFIG": "Configuração"
"CONFIG": "Configuração",
"VULNERABILITY": "Vulnerability",
"GARBAGE_COLLECTION": "Garbage Collection"
},
"LOGS": "Logs",
"TASKS": "Tasks",
"API_EXPLORER": "API EXPLORER"
},
"USER": {
@ -649,6 +658,7 @@
"AUTH_MODE_LDAP": "LDAP",
"AUTH_MODE_UAA": "UAA",
"AUTH_MODE_HTTP": "http_auth",
"AUTH_MODE_OIDC": "OIDC",
"SCOPE_BASE": "Base",
"SCOPE_ONE_LEVEL": "OneLevel",
"SCOPE_SUBTREE": "Subtree",
@ -711,6 +721,17 @@
"ALWAYS_ONBOARD": "Sempre Onboard",
"VERIFY_CERT": "Verificar certificado de Authentication"
},
"OIDC": {
"OIDC_PROVIDER": "OIDC Fornecedor",
"ENDPOINT": "OIDC Endpoint",
"CLIENT_ID": "ID de cliente OIDC",
"CLIENTSECRET": "OIDC Client Secret",
"SCOPE": "Escopo OIDC",
"OIDCSKIPCERTVERIFY": "Verificar certificado OIDC",
"OIDC_SETNAME": "Definir o Utilizador OIDC",
"OIDC_SETNAMECONTENT": "Você deve Criar um Nome de usuário do Porto a primeira vez que autenticar através de um terceiro (OIDC). Isto será usado Dentro de Harbor para ser associado a projetos, papéis, etc.",
"OIDC_USERNAME": "Utilizador"
},
"SCANNING": {
"TRIGGER_SCAN_ALL_SUCCESS": "Disparo de análise geral efetuado com sucesso!",
"TRIGGER_SCAN_ALL_FAIL": "Falha ao disparar análise geral com erro: {{error}",
@ -925,6 +946,7 @@
"ON": "em",
"AT": "em",
"GC_NOW": "GC AGORA",
"JOB_HISTORY": "GC History",
"JOB_LIST":"Lista de tarefas de Limpeza",
"JOB_ID":"ID DA TAREFA",
"TRIGGER_TYPE": "TIPO DE DISPARO",

View File

@ -22,6 +22,7 @@
"OK": "确定",
"DELETE": "删除",
"LOG_IN": "登录",
"LOG_IN_OIDC": "通过OIDC提供商登录",
"SIGN_UP_LINK": "注册账号",
"SIGN_UP": "注册",
"CONFIRM": "确定",
@ -72,7 +73,12 @@
"RULE_USER_EXISTING": "名称已经存在。",
"EMPTY": "名称为必填项",
"NONEMPTY": "不能为空",
"ENDPOINT_FORMAT": "Endpoint必须以http或https开头。"
"ENDPOINT_FORMAT": "Endpoint必须以http或https开头。",
"OIDC_ENDPOIT_FORMAT": "Endpoint必须以https开头。",
"OIDC_NAME": "OIDC提供商的名称.",
"OIDC_ENDPOINT": "OIDC服务器的地址.",
"OIDC_SCOPE": "在身份验证期间发送到OIDC服务器的scope。它必须包含“openid”和“offline_access”。如果您使用Google请从此字段中删除“脱机访问”。",
"OIDC_SKIPCERTVERIFY": "如果您的OIDC服务器是通过自签名证书托管的请选中此框。"
},
"PLACEHOLDER": {
"CURRENT_PWD": "输入当前密码",
@ -124,9 +130,12 @@
"GROUP": "组管理",
"REGISTRY": "仓库管理",
"REPLICATION": "复制管理",
"CONFIG": "配置管理"
"CONFIG": "配置管理",
"VULNERABILITY": "漏洞",
"GARBAGE_COLLECTION": "垃圾清理"
},
"LOGS": "日志",
"TASKS": "任务",
"API_EXPLORER": "API控制中心"
},
"USER": {
@ -654,6 +663,7 @@
"AUTH_MODE_LDAP": "LDAP",
"AUTH_MODE_UAA": "UAA",
"AUTH_MODE_HTTP": "http_auth",
"AUTH_MODE_OIDC": "OIDC",
"SCOPE_BASE": "本层",
"SCOPE_ONE_LEVEL": "下一层",
"SCOPE_SUBTREE": "子树",
@ -716,6 +726,17 @@
"ALWAYS_ONBOARD": "Always Onboard",
"VERIFY_CERT": "Authentication验证证书"
},
"OIDC": {
"OIDC_PROVIDER": "OIDC 供应商",
"ENDPOINT": "OIDC Endpoint",
"CLIENT_ID": "OIDC 客户端标识",
"CLIENTSECRET": "OIDC 客户端密码",
"SCOPE": "OIDC scope",
"OIDCSKIPCERTVERIFY": "OIDC 验证证书",
"OIDC_SETNAME": "设置OIDC用户名",
"OIDC_SETNAMECONTENT": "在通过第三方OIDC进行身份验证时您必须第一次创建一个Harbor用户名。这将在端口中用于与项目、角色等关联。",
"OIDC_USERNAME": "用户名"
},
"SCANNING": {
"TRIGGER_SCAN_ALL_SUCCESS": "启动扫描所有镜像任务成功!",
"TRIGGER_SCAN_ALL_FAIL": "启动扫描所有镜像任务失败:{{error}",

View File

@ -34,7 +34,7 @@ func TestMain(m *testing.M) {
"job_log", "project", "project_member", "project_metadata", "properties", "registry",
"replication_immediate_trigger", "replication_job", "replication_policy", "replication_policy_ng",
"replication_target", "repository", "robot", "role", "schema_migrations", "user_group",
"replication_execution", "replication_task", "replication_schedule_job";`,
"replication_execution", "replication_task", "replication_schedule_job", "oidc_user";`,
`DROP FUNCTION "update_update_time_at_column"();`,
}
dao.PrepareTestData(clearSqls, nil)

2
src/vendor/github.com/coreos/go-oidc/.gitignore generated vendored Normal file
View File

@ -0,0 +1,2 @@
/bin
/gopath

16
src/vendor/github.com/coreos/go-oidc/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,16 @@
language: go
go:
- 1.7.5
- 1.8
install:
- go get -v -t github.com/coreos/go-oidc/...
- go get golang.org/x/tools/cmd/cover
- go get github.com/golang/lint/golint
script:
- ./test
notifications:
email: false

71
src/vendor/github.com/coreos/go-oidc/CONTRIBUTING.md generated vendored Normal file
View File

@ -0,0 +1,71 @@
# How to Contribute
CoreOS projects are [Apache 2.0 licensed](LICENSE) and accept contributions via
GitHub pull requests. This document outlines some of the conventions on
development workflow, commit message formatting, contact points and other
resources to make it easier to get your contribution accepted.
# Certificate of Origin
By contributing to this project you agree to the Developer Certificate of
Origin (DCO). This document was created by the Linux Kernel community and is a
simple statement that you, as a contributor, have the legal right to make the
contribution. See the [DCO](DCO) file for details.
# Email and Chat
The project currently uses the general CoreOS email list and IRC channel:
- Email: [coreos-dev](https://groups.google.com/forum/#!forum/coreos-dev)
- IRC: #[coreos](irc://irc.freenode.org:6667/#coreos) IRC channel on freenode.org
Please avoid emailing maintainers found in the MAINTAINERS file directly. They
are very busy and read the mailing lists.
## Getting Started
- Fork the repository on GitHub
- Read the [README](README.md) for build and test instructions
- Play with the project, submit bugs, submit patches!
## Contribution Flow
This is a rough outline of what a contributor's workflow looks like:
- Create a topic branch from where you want to base your work (usually master).
- Make commits of logical units.
- Make sure your commit messages are in the proper format (see below).
- Push your changes to a topic branch in your fork of the repository.
- Make sure the tests pass, and add any new tests as appropriate.
- Submit a pull request to the original repository.
Thanks for your contributions!
### Format of the Commit Message
We follow a rough convention for commit messages that is designed to answer two
questions: what changed and why. The subject line should feature the what and
the body of the commit should describe the why.
```
scripts: add the test-cluster command
this uses tmux to setup a test cluster that you can easily kill and
start for debugging.
Fixes #38
```
The format can be described more formally as follows:
```
<subsystem>: <what changed>
<BLANK LINE>
<why this change was made>
<BLANK LINE>
<footer>
```
The first line is the subject and should be no longer than 70 characters, the
second line is always blank, and other lines should be wrapped at 80 characters.
This allows the message to be easier to read on GitHub as well as in various
git tools.

36
src/vendor/github.com/coreos/go-oidc/DCO generated vendored Normal file
View File

@ -0,0 +1,36 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.

202
src/vendor/github.com/coreos/go-oidc/LICENSE generated vendored Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

2
src/vendor/github.com/coreos/go-oidc/MAINTAINERS generated vendored Normal file
View File

@ -0,0 +1,2 @@
Eric Chiang <echiang@redhat.com> (@ericchiang)
Rithu Leena John <rjohn@redhat.com> (@rithujohn191)

5
src/vendor/github.com/coreos/go-oidc/NOTICE generated vendored Normal file
View File

@ -0,0 +1,5 @@
CoreOS Project
Copyright 2014 CoreOS, Inc
This product includes software developed at CoreOS, Inc.
(http://www.coreos.com/).

72
src/vendor/github.com/coreos/go-oidc/README.md generated vendored Normal file
View File

@ -0,0 +1,72 @@
# go-oidc
[![GoDoc](https://godoc.org/github.com/coreos/go-oidc?status.svg)](https://godoc.org/github.com/coreos/go-oidc)
[![Build Status](https://travis-ci.org/coreos/go-oidc.png?branch=master)](https://travis-ci.org/coreos/go-oidc)
## OpenID Connect support for Go
This package enables OpenID Connect support for the [golang.org/x/oauth2](https://godoc.org/golang.org/x/oauth2) package.
```go
provider, err := oidc.NewProvider(ctx, "https://accounts.google.com")
if err != nil {
// handle error
}
// Configure an OpenID Connect aware OAuth2 client.
oauth2Config := oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
// Discovery returns the OAuth2 endpoints.
Endpoint: provider.Endpoint(),
// "openid" is a required scope for OpenID Connect flows.
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
```
OAuth2 redirects are unchanged.
```go
func handleRedirect(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
}
```
The on responses, the provider can be used to verify ID Tokens.
```go
var verifier = provider.Verifier(&oidc.Config{ClientID: clientID})
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
// Verify state and errors.
oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
// handle error
}
// Extract the ID Token from OAuth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
// handle missing token
}
// Parse and verify ID Token payload.
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
// handle error
}
// Extract custom claims
var claims struct {
Email string `json:"email"`
Verified bool `json:"email_verified"`
}
if err := idToken.Claims(&claims); err != nil {
// handle error
}
}
```

View File

@ -0,0 +1,61 @@
## CoreOS Community Code of Conduct
### Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating
documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free
experience for everyone, regardless of level of experience, gender, gender
identity and expression, sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing others' private information, such as physical or electronic addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct. By adopting this Code of Conduct,
project maintainers commit themselves to fairly and consistently applying these
principles to every aspect of managing this project. Project maintainers who do
not follow or enforce the Code of Conduct may be permanently removed from the
project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting a project maintainer, Brandon Philips
<brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.
This Code of Conduct is adapted from the Contributor Covenant
(http://contributor-covenant.org), version 1.2.0, available at
http://contributor-covenant.org/version/1/2/0/
### CoreOS Events Code of Conduct
CoreOS events are working conferences intended for professional networking and
collaboration in the CoreOS community. Attendees are expected to behave
according to professional standards and in accordance with their employers
policies on appropriate workplace behavior.
While at CoreOS events or related social networking opportunities, attendees
should not engage in discriminatory or offensive speech or actions including
but not limited to gender, sexuality, race, age, disability, or religion.
Speakers should be especially aware of these concerns.
CoreOS does not condone any statements by speakers contrary to these standards.
CoreOS reserves the right to deny entrance and/or eject from an event (without
refund) any individual found to be engaging in discriminatory or offensive
speech or actions.
Please bring any concerns to the immediate attention of designated on-site
staff, Brandon Philips <brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.

20
src/vendor/github.com/coreos/go-oidc/jose.go generated vendored Normal file
View File

@ -0,0 +1,20 @@
// +build !golint
// Don't lint this file. We don't want to have to add a comment to each constant.
package oidc
const (
// JOSE asymmetric signing algorithm values as defined by RFC 7518
//
// see: https://tools.ietf.org/html/rfc7518#section-3.1
RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256
RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384
RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512
ES256 = "ES256" // ECDSA using P-256 and SHA-256
ES384 = "ES384" // ECDSA using P-384 and SHA-384
ES512 = "ES512" // ECDSA using P-521 and SHA-512
PS256 = "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256
PS384 = "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384
PS512 = "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512
)

228
src/vendor/github.com/coreos/go-oidc/jwks.go generated vendored Normal file
View File

@ -0,0 +1,228 @@
package oidc
import (
"context"
"errors"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
"github.com/pquerna/cachecontrol"
jose "gopkg.in/square/go-jose.v2"
)
// keysExpiryDelta is the allowed clock skew between a client and the OpenID Connect
// server.
//
// When keys expire, they are valid for this amount of time after.
//
// If the keys have not expired, and an ID Token claims it was signed by a key not in
// the cache, if and only if the keys expire in this amount of time, the keys will be
// updated.
const keysExpiryDelta = 30 * time.Second
// NewRemoteKeySet returns a KeySet that can validate JSON web tokens by using HTTP
// GETs to fetch JSON web token sets hosted at a remote URL. This is automatically
// used by NewProvider using the URLs returned by OpenID Connect discovery, but is
// exposed for providers that don't support discovery or to prevent round trips to the
// discovery URL.
//
// The returned KeySet is a long lived verifier that caches keys based on cache-control
// headers. Reuse a common remote key set instead of creating new ones as needed.
//
// The behavior of the returned KeySet is undefined once the context is canceled.
func NewRemoteKeySet(ctx context.Context, jwksURL string) KeySet {
return newRemoteKeySet(ctx, jwksURL, time.Now)
}
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *remoteKeySet {
if now == nil {
now = time.Now
}
return &remoteKeySet{jwksURL: jwksURL, ctx: ctx, now: now}
}
type remoteKeySet struct {
jwksURL string
ctx context.Context
now func() time.Time
// guard all other fields
mu sync.Mutex
// inflight suppresses parallel execution of updateKeys and allows
// multiple goroutines to wait for its result.
inflight *inflight
// A set of cached keys and their expiry.
cachedKeys []jose.JSONWebKey
expiry time.Time
}
// inflight is used to wait on some in-flight request from multiple goroutines.
type inflight struct {
doneCh chan struct{}
keys []jose.JSONWebKey
err error
}
func newInflight() *inflight {
return &inflight{doneCh: make(chan struct{})}
}
// wait returns a channel that multiple goroutines can receive on. Once it returns
// a value, the inflight request is done and result() can be inspected.
func (i *inflight) wait() <-chan struct{} {
return i.doneCh
}
// done can only be called by a single goroutine. It records the result of the
// inflight request and signals other goroutines that the result is safe to
// inspect.
func (i *inflight) done(keys []jose.JSONWebKey, err error) {
i.keys = keys
i.err = err
close(i.doneCh)
}
// result cannot be called until the wait() channel has returned a value.
func (i *inflight) result() ([]jose.JSONWebKey, error) {
return i.keys, i.err
}
func (r *remoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
jws, err := jose.ParseSigned(jwt)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
return r.verify(ctx, jws)
}
func (r *remoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
// We don't support JWTs signed with multiple signatures.
keyID := ""
for _, sig := range jws.Signatures {
keyID = sig.Header.KeyID
break
}
keys, expiry := r.keysFromCache()
// Don't check expiry yet. This optimizes for when the provider is unavailable.
for _, key := range keys {
if keyID == "" || key.KeyID == keyID {
if payload, err := jws.Verify(&key); err == nil {
return payload, nil
}
}
}
if !r.now().Add(keysExpiryDelta).After(expiry) {
// Keys haven't expired, don't refresh.
return nil, errors.New("failed to verify id token signature")
}
keys, err := r.keysFromRemote(ctx)
if err != nil {
return nil, fmt.Errorf("fetching keys %v", err)
}
for _, key := range keys {
if keyID == "" || key.KeyID == keyID {
if payload, err := jws.Verify(&key); err == nil {
return payload, nil
}
}
}
return nil, errors.New("failed to verify id token signature")
}
func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey, expiry time.Time) {
r.mu.Lock()
defer r.mu.Unlock()
return r.cachedKeys, r.expiry
}
// keysFromRemote syncs the key set from the remote set, records the values in the
// cache, and returns the key set.
func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) {
// Need to lock to inspect the inflight request field.
r.mu.Lock()
// If there's not a current inflight request, create one.
if r.inflight == nil {
r.inflight = newInflight()
// This goroutine has exclusive ownership over the current inflight
// request. It releases the resource by nil'ing the inflight field
// once the goroutine is done.
go func() {
// Sync keys and finish inflight when that's done.
keys, expiry, err := r.updateKeys()
r.inflight.done(keys, err)
// Lock to update the keys and indicate that there is no longer an
// inflight request.
r.mu.Lock()
defer r.mu.Unlock()
if err == nil {
r.cachedKeys = keys
r.expiry = expiry
}
// Free inflight so a different request can run.
r.inflight = nil
}()
}
inflight := r.inflight
r.mu.Unlock()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-inflight.wait():
return inflight.result()
}
}
func (r *remoteKeySet) updateKeys() ([]jose.JSONWebKey, time.Time, error) {
req, err := http.NewRequest("GET", r.jwksURL, nil)
if err != nil {
return nil, time.Time{}, fmt.Errorf("oidc: can't create request: %v", err)
}
resp, err := doRequest(r.ctx, req)
if err != nil {
return nil, time.Time{}, fmt.Errorf("oidc: get keys failed %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, time.Time{}, fmt.Errorf("unable to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, time.Time{}, fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body)
}
var keySet jose.JSONWebKeySet
err = unmarshalResp(resp, body, &keySet)
if err != nil {
return nil, time.Time{}, fmt.Errorf("oidc: failed to decode keys: %v %s", err, body)
}
// If the server doesn't provide cache control headers, assume the
// keys expire immediately.
expiry := r.now()
_, e, err := cachecontrol.CachableResponse(req, resp, cachecontrol.Options{})
if err == nil && e.After(expiry) {
expiry = e
}
return keySet.Keys, expiry, nil
}

374
src/vendor/github.com/coreos/go-oidc/oidc.go generated vendored Normal file
View File

@ -0,0 +1,374 @@
// Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package.
package oidc
import (
"context"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"hash"
"io/ioutil"
"mime"
"net/http"
"strings"
"time"
"golang.org/x/oauth2"
jose "gopkg.in/square/go-jose.v2"
)
const (
// ScopeOpenID is the mandatory scope for all OpenID Connect OAuth2 requests.
ScopeOpenID = "openid"
// ScopeOfflineAccess is an optional scope defined by OpenID Connect for requesting
// OAuth2 refresh tokens.
//
// Support for this scope differs between OpenID Connect providers. For instance
// Google rejects it, favoring appending "access_type=offline" as part of the
// authorization request instead.
//
// See: https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
ScopeOfflineAccess = "offline_access"
)
var (
errNoAtHash = errors.New("id token did not have an access token hash")
errInvalidAtHash = errors.New("access token hash does not match value in ID token")
)
// ClientContext returns a new Context that carries the provided HTTP client.
//
// This method sets the same context key used by the golang.org/x/oauth2 package,
// so the returned context works for that package too.
//
// myClient := &http.Client{}
// ctx := oidc.ClientContext(parentContext, myClient)
//
// // This will use the custom client
// provider, err := oidc.NewProvider(ctx, "https://accounts.example.com")
//
func ClientContext(ctx context.Context, client *http.Client) context.Context {
return context.WithValue(ctx, oauth2.HTTPClient, client)
}
func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
client := http.DefaultClient
if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
client = c
}
return client.Do(req.WithContext(ctx))
}
// Provider represents an OpenID Connect server's configuration.
type Provider struct {
issuer string
authURL string
tokenURL string
userInfoURL string
// Raw claims returned by the server.
rawClaims []byte
remoteKeySet KeySet
}
type cachedKeys struct {
keys []jose.JSONWebKey
expiry time.Time
}
type providerJSON struct {
Issuer string `json:"issuer"`
AuthURL string `json:"authorization_endpoint"`
TokenURL string `json:"token_endpoint"`
JWKSURL string `json:"jwks_uri"`
UserInfoURL string `json:"userinfo_endpoint"`
}
// NewProvider uses the OpenID Connect discovery mechanism to construct a Provider.
//
// The issuer is the URL identifier for the service. For example: "https://accounts.google.com"
// or "https://login.salesforce.com".
func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration"
req, err := http.NewRequest("GET", wellKnown, nil)
if err != nil {
return nil, err
}
resp, err := doRequest(ctx, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unable to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: %s", resp.Status, body)
}
var p providerJSON
err = unmarshalResp(resp, body, &p)
if err != nil {
return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
}
if p.Issuer != issuer {
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer)
}
return &Provider{
issuer: p.Issuer,
authURL: p.AuthURL,
tokenURL: p.TokenURL,
userInfoURL: p.UserInfoURL,
rawClaims: body,
remoteKeySet: NewRemoteKeySet(ctx, p.JWKSURL),
}, nil
}
// Claims unmarshals raw fields returned by the server during discovery.
//
// var claims struct {
// ScopesSupported []string `json:"scopes_supported"`
// ClaimsSupported []string `json:"claims_supported"`
// }
//
// if err := provider.Claims(&claims); err != nil {
// // handle unmarshaling error
// }
//
// For a list of fields defined by the OpenID Connect spec see:
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
func (p *Provider) Claims(v interface{}) error {
if p.rawClaims == nil {
return errors.New("oidc: claims not set")
}
return json.Unmarshal(p.rawClaims, v)
}
// Endpoint returns the OAuth2 auth and token endpoints for the given provider.
func (p *Provider) Endpoint() oauth2.Endpoint {
return oauth2.Endpoint{AuthURL: p.authURL, TokenURL: p.tokenURL}
}
// UserInfo represents the OpenID Connect userinfo claims.
type UserInfo struct {
Subject string `json:"sub"`
Profile string `json:"profile"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
claims []byte
}
// Claims unmarshals the raw JSON object claims into the provided object.
func (u *UserInfo) Claims(v interface{}) error {
if u.claims == nil {
return errors.New("oidc: claims not set")
}
return json.Unmarshal(u.claims, v)
}
// UserInfo uses the token source to query the provider's user info endpoint.
func (p *Provider) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*UserInfo, error) {
if p.userInfoURL == "" {
return nil, errors.New("oidc: user info endpoint is not supported by this provider")
}
req, err := http.NewRequest("GET", p.userInfoURL, nil)
if err != nil {
return nil, fmt.Errorf("oidc: create GET request: %v", err)
}
token, err := tokenSource.Token()
if err != nil {
return nil, fmt.Errorf("oidc: get access token: %v", err)
}
token.SetAuthHeader(req)
resp, err := doRequest(ctx, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: %s", resp.Status, body)
}
var userInfo UserInfo
if err := json.Unmarshal(body, &userInfo); err != nil {
return nil, fmt.Errorf("oidc: failed to decode userinfo: %v", err)
}
userInfo.claims = body
return &userInfo, nil
}
// IDToken is an OpenID Connect extension that provides a predictable representation
// of an authorization event.
//
// The ID Token only holds fields OpenID Connect requires. To access additional
// claims returned by the server, use the Claims method.
type IDToken struct {
// The URL of the server which issued this token. OpenID Connect
// requires this value always be identical to the URL used for
// initial discovery.
//
// Note: Because of a known issue with Google Accounts' implementation
// this value may differ when using Google.
//
// See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo
Issuer string
// The client ID, or set of client IDs, that this token is issued for. For
// common uses, this is the client that initialized the auth flow.
//
// This package ensures the audience contains an expected value.
Audience []string
// A unique string which identifies the end user.
Subject string
// Expiry of the token. Ths package will not process tokens that have
// expired unless that validation is explicitly turned off.
Expiry time.Time
// When the token was issued by the provider.
IssuedAt time.Time
// Initial nonce provided during the authentication redirect.
//
// This package does NOT provided verification on the value of this field
// and it's the user's responsibility to ensure it contains a valid value.
Nonce string
// at_hash claim, if set in the ID token. Callers can verify an access token
// that corresponds to the ID token using the VerifyAccessToken method.
AccessTokenHash string
// signature algorithm used for ID token, needed to compute a verification hash of an
// access token
sigAlgorithm string
// Raw payload of the id_token.
claims []byte
}
// Claims unmarshals the raw JSON payload of the ID Token into a provided struct.
//
// idToken, err := idTokenVerifier.Verify(rawIDToken)
// if err != nil {
// // handle error
// }
// var claims struct {
// Email string `json:"email"`
// EmailVerified bool `json:"email_verified"`
// }
// if err := idToken.Claims(&claims); err != nil {
// // handle error
// }
//
func (i *IDToken) Claims(v interface{}) error {
if i.claims == nil {
return errors.New("oidc: claims not set")
}
return json.Unmarshal(i.claims, v)
}
// VerifyAccessToken verifies that the hash of the access token that corresponds to the iD token
// matches the hash in the id token. It returns an error if the hashes don't match.
// It is the caller's responsibility to ensure that the optional access token hash is present for the ID token
// before calling this method. See https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
func (i *IDToken) VerifyAccessToken(accessToken string) error {
if i.AccessTokenHash == "" {
return errNoAtHash
}
var h hash.Hash
switch i.sigAlgorithm {
case RS256, ES256, PS256:
h = sha256.New()
case RS384, ES384, PS384:
h = sha512.New384()
case RS512, ES512, PS512:
h = sha512.New()
default:
return fmt.Errorf("oidc: unsupported signing algorithm %q", i.sigAlgorithm)
}
h.Write([]byte(accessToken)) // hash documents that Write will never return an error
sum := h.Sum(nil)[:h.Size()/2]
actual := base64.RawURLEncoding.EncodeToString(sum)
if actual != i.AccessTokenHash {
return errInvalidAtHash
}
return nil
}
type idToken struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience audience `json:"aud"`
Expiry jsonTime `json:"exp"`
IssuedAt jsonTime `json:"iat"`
Nonce string `json:"nonce"`
AtHash string `json:"at_hash"`
}
type audience []string
func (a *audience) UnmarshalJSON(b []byte) error {
var s string
if json.Unmarshal(b, &s) == nil {
*a = audience{s}
return nil
}
var auds []string
if err := json.Unmarshal(b, &auds); err != nil {
return err
}
*a = audience(auds)
return nil
}
type jsonTime time.Time
func (j *jsonTime) UnmarshalJSON(b []byte) error {
var n json.Number
if err := json.Unmarshal(b, &n); err != nil {
return err
}
var unix int64
if t, err := n.Int64(); err == nil {
unix = t
} else {
f, err := n.Float64()
if err != nil {
return err
}
unix = int64(f)
}
*j = jsonTime(time.Unix(unix, 0))
return nil
}
func unmarshalResp(r *http.Response, body []byte, v interface{}) error {
err := json.Unmarshal(body, &v)
if err == nil {
return nil
}
ct := r.Header.Get("Content-Type")
mediaType, _, parseErr := mime.ParseMediaType(ct)
if parseErr == nil && mediaType == "application/json" {
return fmt.Errorf("got Content-Type = application/json, but could not unmarshal as JSON: %v", err)
}
return fmt.Errorf("expected Content-Type = application/json, got %q: %v", ct, err)
}

16
src/vendor/github.com/coreos/go-oidc/test generated vendored Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
set -e
# Filter out any files with a !golint build tag.
LINTABLE=$( go list -tags=golint -f '
{{- range $i, $file := .GoFiles -}}
{{ $file }} {{ end }}
{{ range $i, $file := .TestGoFiles -}}
{{ $file }} {{ end }}' github.com/coreos/go-oidc )
go test -v -i -race github.com/coreos/go-oidc/...
go test -v -race github.com/coreos/go-oidc/...
golint -set_exit_status $LINTABLE
go vet github.com/coreos/go-oidc/...
go build -v ./example/...

243
src/vendor/github.com/coreos/go-oidc/verify.go generated vendored Normal file
View File

@ -0,0 +1,243 @@
package oidc
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"golang.org/x/oauth2"
jose "gopkg.in/square/go-jose.v2"
)
const (
issuerGoogleAccounts = "https://accounts.google.com"
issuerGoogleAccountsNoScheme = "accounts.google.com"
)
// KeySet is a set of publc JSON Web Keys that can be used to validate the signature
// of JSON web tokens. This is expected to be backed by a remote key set through
// provider metadata discovery or an in-memory set of keys delivered out-of-band.
type KeySet interface {
// VerifySignature parses the JSON web token, verifies the signature, and returns
// the raw payload. Header and claim fields are validated by other parts of the
// package. For example, the KeySet does not need to check values such as signature
// algorithm, issuer, and audience since the IDTokenVerifier validates these values
// independently.
//
// If VerifySignature makes HTTP requests to verify the token, it's expected to
// use any HTTP client associated with the context through ClientContext.
VerifySignature(ctx context.Context, jwt string) (payload []byte, err error)
}
// IDTokenVerifier provides verification for ID Tokens.
type IDTokenVerifier struct {
keySet KeySet
config *Config
issuer string
}
// NewVerifier returns a verifier manually constructed from a key set and issuer URL.
//
// It's easier to use provider discovery to construct an IDTokenVerifier than creating
// one directly. This method is intended to be used with provider that don't support
// metadata discovery, or avoiding round trips when the key set URL is already known.
//
// This constructor can be used to create a verifier directly using the issuer URL and
// JSON Web Key Set URL without using discovery:
//
// keySet := oidc.NewRemoteKeySet(ctx, "https://www.googleapis.com/oauth2/v3/certs")
// verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config)
//
// Since KeySet is an interface, this constructor can also be used to supply custom
// public key sources. For example, if a user wanted to supply public keys out-of-band
// and hold them statically in-memory:
//
// // Custom KeySet implementation.
// keySet := newStatisKeySet(publicKeys...)
//
// // Verifier uses the custom KeySet implementation.
// verifier := oidc.NewVerifier("https://auth.example.com", keySet, config)
//
func NewVerifier(issuerURL string, keySet KeySet, config *Config) *IDTokenVerifier {
return &IDTokenVerifier{keySet: keySet, config: config, issuer: issuerURL}
}
// Config is the configuration for an IDTokenVerifier.
type Config struct {
// Expected audience of the token. For a majority of the cases this is expected to be
// the ID of the client that initialized the login flow. It may occasionally differ if
// the provider supports the authorizing party (azp) claim.
//
// If not provided, users must explicitly set SkipClientIDCheck.
ClientID string
// If specified, only this set of algorithms may be used to sign the JWT.
//
// Since many providers only support RS256, SupportedSigningAlgs defaults to this value.
SupportedSigningAlgs []string
// If true, no ClientID check performed. Must be true if ClientID field is empty.
SkipClientIDCheck bool
// If true, token expiry is not checked.
SkipExpiryCheck bool
// Time function to check Token expiry. Defaults to time.Now
Now func() time.Time
}
// Verifier returns an IDTokenVerifier that uses the provider's key set to verify JWTs.
//
// The returned IDTokenVerifier is tied to the Provider's context and its behavior is
// undefined once the Provider's context is canceled.
func (p *Provider) Verifier(config *Config) *IDTokenVerifier {
return NewVerifier(p.issuer, p.remoteKeySet, config)
}
func parseJWT(p string) ([]byte, error) {
parts := strings.Split(p, ".")
if len(parts) < 2 {
return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts))
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err)
}
return payload, nil
}
func contains(sli []string, ele string) bool {
for _, s := range sli {
if s == ele {
return true
}
}
return false
}
// Verify parses a raw ID Token, verifies it's been signed by the provider, preforms
// any additional checks depending on the Config, and returns the payload.
//
// Verify does NOT do nonce validation, which is the callers responsibility.
//
// See: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
//
// oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
// if err != nil {
// // handle error
// }
//
// // Extract the ID Token from oauth2 token.
// rawIDToken, ok := oauth2Token.Extra("id_token").(string)
// if !ok {
// // handle error
// }
//
// token, err := verifier.Verify(ctx, rawIDToken)
//
func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) {
jws, err := jose.ParseSigned(rawIDToken)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
// Throw out tokens with invalid claims before trying to verify the token. This lets
// us do cheap checks before possibly re-syncing keys.
payload, err := parseJWT(rawIDToken)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
var token idToken
if err := json.Unmarshal(payload, &token); err != nil {
return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err)
}
t := &IDToken{
Issuer: token.Issuer,
Subject: token.Subject,
Audience: []string(token.Audience),
Expiry: time.Time(token.Expiry),
IssuedAt: time.Time(token.IssuedAt),
Nonce: token.Nonce,
AccessTokenHash: token.AtHash,
claims: payload,
}
// Check issuer.
if t.Issuer != v.issuer {
// Google sometimes returns "accounts.google.com" as the issuer claim instead of
// the required "https://accounts.google.com". Detect this case and allow it only
// for Google.
//
// We will not add hooks to let other providers go off spec like this.
if !(v.issuer == issuerGoogleAccounts && t.Issuer == issuerGoogleAccountsNoScheme) {
return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.issuer, t.Issuer)
}
}
// If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty.
//
// This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party).
if !v.config.SkipClientIDCheck {
if v.config.ClientID != "" {
if !contains(t.Audience, v.config.ClientID) {
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience)
}
} else {
return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set")
}
}
// If a SkipExpiryCheck is false, make sure token is not expired.
if !v.config.SkipExpiryCheck {
now := time.Now
if v.config.Now != nil {
now = v.config.Now
}
if t.Expiry.Before(now()) {
return nil, fmt.Errorf("oidc: token is expired (Token Expiry: %v)", t.Expiry)
}
}
switch len(jws.Signatures) {
case 0:
return nil, fmt.Errorf("oidc: id token not signed")
case 1:
default:
return nil, fmt.Errorf("oidc: multiple signatures on id token not supported")
}
sig := jws.Signatures[0]
supportedSigAlgs := v.config.SupportedSigningAlgs
if len(supportedSigAlgs) == 0 {
supportedSigAlgs = []string{RS256}
}
if !contains(supportedSigAlgs, sig.Header.Algorithm) {
return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm)
}
t.sigAlgorithm = sig.Header.Algorithm
gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("failed to verify signature: %v", err)
}
// Ensure that the payload returned by the square actually matches the payload parsed earlier.
if !bytes.Equal(gotPayload, payload) {
return nil, errors.New("oidc: internal error, payload parsed did not match previous payload")
}
return t, nil
}
// Nonce returns an auth code option which requires the ID Token created by the
// OpenID Connect provider to contain the specified nonce.
func Nonce(nonce string) oauth2.AuthCodeOption {
return oauth2.SetAuthURLParam("nonce", nonce)
}

15
src/vendor/github.com/gogo/protobuf/AUTHORS generated vendored Normal file
View File

@ -0,0 +1,15 @@
# This is the official list of GoGo authors for copyright purposes.
# This file is distinct from the CONTRIBUTORS file, which
# lists people. For example, employees are listed in CONTRIBUTORS,
# but not in AUTHORS, because the employer holds the copyright.
# Names should be added to this file as one of
# Organization's name
# Individual's name <submission email address>
# Individual's name <submission email address> <email2> <emailN>
# Please keep the list sorted.
Sendgrid, Inc
Vastech SA (PTY) LTD
Walter Schulze <awalterschulze@gmail.com>

23
src/vendor/github.com/gogo/protobuf/CONTRIBUTORS generated vendored Normal file
View File

@ -0,0 +1,23 @@
Anton Povarov <anton.povarov@gmail.com>
Brian Goff <cpuguy83@gmail.com>
Clayton Coleman <ccoleman@redhat.com>
Denis Smirnov <denis.smirnov.91@gmail.com>
DongYun Kang <ceram1000@gmail.com>
Dwayne Schultz <dschultz@pivotal.io>
Georg Apitz <gapitz@pivotal.io>
Gustav Paul <gustav.paul@gmail.com>
Johan Brandhorst <johan.brandhorst@gmail.com>
John Shahid <jvshahid@gmail.com>
John Tuley <john@tuley.org>
Laurent <laurent@adyoulike.com>
Patrick Lee <patrick@dropbox.com>
Peter Edge <peter.edge@gmail.com>
Roger Johansson <rogeralsing@gmail.com>
Sam Nguyen <sam.nguyen@sendgrid.com>
Sergio Arbeo <serabe@gmail.com>
Stephen J Day <stephen.day@docker.com>
Tamir Duberstein <tamird@gmail.com>
Todd Eisenberger <teisenberger@dropbox.com>
Tormod Erevik Lea <tormodlea@gmail.com>
Vyacheslav Kim <kane@sendgrid.com>
Walter Schulze <awalterschulze@gmail.com>

View File

@ -0,0 +1,5 @@
The contributors to the Go protobuf repository:
# This source code was written by the Go contributors.
# The master list of contributors is in the main Go distribution,
# visible at http://tip.golang.org/CONTRIBUTORS.

35
src/vendor/github.com/gogo/protobuf/LICENSE generated vendored Normal file
View File

@ -0,0 +1,35 @@
Copyright (c) 2013, The GoGo Authors. All rights reserved.
Protocol Buffers for Go with Gadgets
Go support for Protocol Buffers - Google's data interchange format
Copyright 2010 The Go Authors. All rights reserved.
https://github.com/golang/protobuf
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Some files were not shown because too many files have changed in this diff Show More