mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-30 06:03:45 +01:00
Merge pull request #7299 from ywk253100/190404_sync
Sync with master branch
This commit is contained in:
commit
58a73de3e5
@ -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.
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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
210
src/Gopkg.lock
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{}},
|
||||
|
||||
|
@ -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 ...
|
||||
|
@ -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
168
src/common/dao/oidc_user.go
Normal 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
|
||||
}
|
186
src/common/dao/oidc_user_test.go
Normal file
186
src/common/dao/oidc_user_test.go
Normal 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)
|
||||
}()
|
||||
|
||||
}
|
@ -38,5 +38,6 @@ func init() {
|
||||
new(UserGroup),
|
||||
new(AdminJob),
|
||||
new(JobLog),
|
||||
new(Robot))
|
||||
new(Robot),
|
||||
new(OIDCUser))
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
||||
|
20
src/common/models/oidc_user.go
Normal file
20
src/common/models/oidc_user.go
Normal 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"
|
||||
}
|
@ -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 ...
|
||||
|
188
src/common/utils/oidc/helper.go
Normal file
188
src/common/utils/oidc/helper.go
Normal 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)
|
||||
}
|
98
src/common/utils/oidc/helper_test.go
Normal file
98
src/common/utils/oidc/helper_test.go
Normal 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"))
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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 + `$`)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
|
||||
}
|
||||
|
150
src/core/controllers/oidc.go
Normal file
150
src/core/controllers/oidc.go
Normal 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)
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
89
src/core/filter/test/server.go
Normal file
89
src/core/filter/test/server.go
Normal 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
|
||||
}
|
@ -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{})
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -0,0 +1,4 @@
|
||||
.history-header {
|
||||
color: #000;
|
||||
margin:20px 0 6px 0;
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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";
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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: [
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -60,3 +60,6 @@
|
||||
background:transparent;
|
||||
}
|
||||
}
|
||||
.title{
|
||||
margin-bottom: 50px;
|
||||
}
|
@ -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: [
|
||||
],
|
||||
|
@ -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">
|
||||
|
@ -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()"
|
||||
|
@ -9,7 +9,9 @@ clr-tooltip {
|
||||
.top-1 {
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.top-7 {
|
||||
top: -7px;
|
||||
.top-1px {
|
||||
top: 1px;
|
||||
}
|
||||
.top-5 {
|
||||
top: -5px;
|
||||
}
|
@ -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);
|
||||
|
@ -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>
|
@ -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);
|
||||
|
23
src/portal/src/app/gc-page/gc-page.component.html
Normal file
23
src/portal/src/app/gc-page/gc-page.component.html
Normal 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>
|
3
src/portal/src/app/gc-page/gc-page.component.scss
Normal file
3
src/portal/src/app/gc-page/gc-page.component.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.gc-title {
|
||||
display: inline-block;
|
||||
}
|
25
src/portal/src/app/gc-page/gc-page.component.spec.ts
Normal file
25
src/portal/src/app/gc-page/gc-page.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
19
src/portal/src/app/gc-page/gc-page.component.ts
Normal file
19
src/portal/src/app/gc-page/gc-page.component.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
43
src/portal/src/app/oidc-onboard/oidc-onboard.component.html
Normal file
43
src/portal/src/app/oidc-onboard/oidc-onboard.component.html
Normal 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>
|
||||
</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>
|
25
src/portal/src/app/oidc-onboard/oidc-onboard.component.scss
Normal file
25
src/portal/src/app/oidc-onboard/oidc-onboard.component.scss
Normal 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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
43
src/portal/src/app/oidc-onboard/oidc-onboard.component.ts
Normal file
43
src/portal/src/app/oidc-onboard/oidc-onboard.component.ts
Normal 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]);
|
||||
}
|
||||
}
|
29
src/portal/src/app/oidc-onboard/oidc-onboard.module.ts
Normal file
29
src/portal/src/app/oidc-onboard/oidc-onboard.module.ts
Normal 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 { }
|
12
src/portal/src/app/oidc-onboard/oidc-onboard.service.spec.ts
Normal file
12
src/portal/src/app/oidc-onboard/oidc-onboard.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
16
src/portal/src/app/oidc-onboard/oidc-onboard.service.ts
Normal file
16
src/portal/src/app/oidc-onboard/oidc-onboard.service.ts
Normal 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)));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
58
src/portal/src/app/shared/route/oidc-guard-active.service.ts
Normal file
58
src/portal/src/app/shared/route/oidc-guard-active.service.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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
|
||||
]
|
||||
|
@ -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>
|
@ -0,0 +1,3 @@
|
||||
.vul-title {
|
||||
display: inline-block;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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}",
|
||||
|
@ -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
2
src/vendor/github.com/coreos/go-oidc/.gitignore
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/bin
|
||||
/gopath
|
16
src/vendor/github.com/coreos/go-oidc/.travis.yml
generated
vendored
Normal file
16
src/vendor/github.com/coreos/go-oidc/.travis.yml
generated
vendored
Normal 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
71
src/vendor/github.com/coreos/go-oidc/CONTRIBUTING.md
generated
vendored
Normal 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
36
src/vendor/github.com/coreos/go-oidc/DCO
generated
vendored
Normal 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
202
src/vendor/github.com/coreos/go-oidc/LICENSE
generated
vendored
Normal 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
2
src/vendor/github.com/coreos/go-oidc/MAINTAINERS
generated
vendored
Normal 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
5
src/vendor/github.com/coreos/go-oidc/NOTICE
generated
vendored
Normal 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
72
src/vendor/github.com/coreos/go-oidc/README.md
generated
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
61
src/vendor/github.com/coreos/go-oidc/code-of-conduct.md
generated
vendored
Normal file
61
src/vendor/github.com/coreos/go-oidc/code-of-conduct.md
generated
vendored
Normal 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 employer’s
|
||||
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
20
src/vendor/github.com/coreos/go-oidc/jose.go
generated
vendored
Normal 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
228
src/vendor/github.com/coreos/go-oidc/jwks.go
generated
vendored
Normal 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
374
src/vendor/github.com/coreos/go-oidc/oidc.go
generated
vendored
Normal 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
16
src/vendor/github.com/coreos/go-oidc/test
generated
vendored
Executable 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
243
src/vendor/github.com/coreos/go-oidc/verify.go
generated
vendored
Normal 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
15
src/vendor/github.com/gogo/protobuf/AUTHORS
generated
vendored
Normal 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
23
src/vendor/github.com/gogo/protobuf/CONTRIBUTORS
generated
vendored
Normal 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>
|
5
src/vendor/github.com/gogo/protobuf/GOLANG_CONTRIBUTORS
generated
vendored
Normal file
5
src/vendor/github.com/gogo/protobuf/GOLANG_CONTRIBUTORS
generated
vendored
Normal 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
35
src/vendor/github.com/gogo/protobuf/LICENSE
generated
vendored
Normal 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
Loading…
Reference in New Issue
Block a user